diff --git a/docs/web/user_guide.md b/docs/web/user_guide.md index 0d6070fccc..d1bc9d0525 100644 --- a/docs/web/user_guide.md +++ b/docs/web/user_guide.md @@ -880,7 +880,9 @@ This mode shows analysis results (in the same format as `results`) does, but from the comparison of two runs. ``` -usage: CodeChecker cmd diff [-h] -b BASE_RUN -n NEW_RUN [--uniqueing {on,off}] +usage: CodeChecker cmd diff [-h] [-b BASE_RUNS [BASE_RUNS ...]] + [-n NEW_RUNS [NEW_RUNS ...]] + [--uniqueing {on,off}] [--report-hash [REPORT_HASH [REPORT_HASH ...]]] [--review-status [REVIEW_STATUS [REVIEW_STATUS ...]]] [--detection-status [DETECTION_STATUS [DETECTION_STATUS ...]]] @@ -903,22 +905,26 @@ Compare two analysis runs to show the results that differ between the two. optional arguments: -h, --help show this help message and exit - -b BASE_RUN, --basename BASE_RUN - The 'base' (left) side of the difference: this - analysis run is used as the initial state in the - comparison. The basename can contain * quantifiers + -b BASE_RUNS [BASE_RUNS ...], --basename BASE_RUNS [BASE_RUNS ...] + The 'base' (left) side of the difference: these + analysis runs are used as the initial state in the + comparison. The parameter can be multiple run names + (on the remote server) or multiple local report + directories (result of the analyze command). In case + of run name the the basename can contain * quantifiers which matches any number of characters (zero or more). So if you have run-a-1, run-a-2 and run-b-1 then "run-a*" selects the first two. - -n NEW_RUN, --newname NEW_RUN - The 'new' (right) side of the difference: this - analysis run is compared to the -b/--basename run. The - parameter can be a run name(on the remote server) or a - local report directory (result of the analyze - command). In case of run name the newname can contain - * quantifiers which matches any number of characters - (zero or more). So if you have run-a-1, run-a-2 and - run-b-1 then "run-a*" selects the first two. + -n NEW_RUNS [NEW_RUNS ...], --newname NEW_RUNS [NEW_RUNS ...] + The 'new' (right) side of the difference: these + analysis runs are compared to the -b/--basename runs. + The parameter can be multiple run names (on the remote + server) or multiple local report directories (result + of the analyze command). In case of run name the + newname can contain * quantifiers which matches any + number of characters (zero or more). So if you have + run-a-1, run-a-2 and run-b-1 then "run-a*" selects the + first two. comparison modes: --new Show results that didn't exist in the 'base' but diff --git a/web/client/codechecker_client/cmd/cmd.py b/web/client/codechecker_client/cmd/cmd.py index d074772b8f..3811f76e24 100644 --- a/web/client/codechecker_client/cmd/cmd.py +++ b/web/client/codechecker_client/cmd/cmd.py @@ -373,38 +373,37 @@ def __register_diff(parser): parser.add_argument('-b', '--basename', type=str, - dest="basename", - metavar='BASE_RUN', - required=True, + nargs='+', + dest="base_run_names", + metavar='BASE_RUNS', default=argparse.SUPPRESS, - help="The 'base' (left) side of the difference: this " - "analysis run is used as the initial state in " - "the comparison. The parameter can be a run name " - "(on the remote server) or a local report " - "directory (result of the analyze command). In " - "case of run name the the basename can contain * " - "quantifiers which matches any number of " - "characters (zero or more). So if you have " - "run-a-1, run-a-2 and run-b-1 " - "then \"run-a*\" selects the first two.") + help="The 'base' (left) side of the difference: these " + "analysis runs are used as the initial state in " + "the comparison. The parameter can be multiple " + "run names (on the remote server) or multiple " + "local report directories (result of the analyze " + "command). In case of run name the the basename " + "can contain * quantifiers which matches any " + "number of characters (zero or more). So if you " + "have run-a-1, run-a-2 and run-b-1 then " + "\"run-a*\" selects the first two.") parser.add_argument('-n', '--newname', type=str, - dest="newname", - metavar='NEW_RUN', - required=True, + nargs='+', + dest="new_run_names", + metavar='NEW_RUNS', default=argparse.SUPPRESS, - help="The 'new' (right) side of the difference: this " - "analysis run is compared to the -b/--basename " - "run. The parameter can be a run name " - "(on the remote server) or a local " - "report directory " - "(result of the analyze command). In case of run " - "name the newname can contain * quantifiers " - "which matches any number of characters " - "(zero or more). So if you have " - "run-a-1, run-a-2 and run-b-1 " - "then \"run-a*\" selects the first two.") + help="The 'new' (right) side of the difference: these " + "analysis runs are compared to the -b/--basename " + "runs. The parameter can be multiple run names " + "(on the remote server) or multiple local " + "report directories (result of the analyze " + "command). In case of run name the newname can " + "contain * quantifiers which matches any number " + "of characters (zero or more). So if you have " + "run-a-1, run-a-2 and run-b-1 then " + "\"run-a*\" selects the first two.") __add_filtering_arguments(parser, DEFAULT_FILTER_VALUES, True) diff --git a/web/client/codechecker_client/cmd_line_client.py b/web/client/codechecker_client/cmd_line_client.py index f4d21b835b..2cdb85a04a 100644 --- a/web/client/codechecker_client/cmd_line_client.py +++ b/web/client/codechecker_client/cmd_line_client.py @@ -497,14 +497,18 @@ def skip_report_dir_result(report): return False - def get_report_dir_results(reportdir): + def get_report_dir_results(report_dirs): + """ Get reports from the given report directories. + + Absolute paths are expected to the given report directories. + """ all_reports = [] processed_path_hashes = set() - for filename in os.listdir(reportdir): - if filename.endswith(".plist"): - file_path = os.path.join(reportdir, filename) - LOG.debug("Parsing: %s", file_path) - try: + for report_dir in report_dirs: + for filename in os.listdir(report_dir): + if filename.endswith(".plist"): + file_path = os.path.join(report_dir, filename) + LOG.debug("Parsing: %s", file_path) files, reports = plist_parser.parse_plist_file(file_path) for report in reports: path_hash = get_report_path_hash(report, files) @@ -523,10 +527,6 @@ def get_report_dir_results(reportdir): report.main['location']['file_name'] = \ files[int(report.main['location']['file'])] all_reports.append(report) - - except Exception as ex: - LOG.error('The generated plist is not valid!') - LOG.error(ex) return all_reports def get_line_from_file(filename, lineno): @@ -613,17 +613,16 @@ def get_diff_type(): return None - def get_diff_local_dir_remote_run(client, report_dir, run_name): + def get_diff_local_dir_remote_run(client, report_dirs, remote_run_names): """ Compares a local report directory with a remote run. """ filtered_reports = [] - report_dir_results = get_report_dir_results( - os.path.abspath(report_dir)) + report_dir_results = get_report_dir_results(report_dirs) suppressed_in_code = get_suppressed_reports(report_dir_results) diff_type = get_diff_type() - run_ids, run_names, _ = process_run_arg(run_name) + run_ids, run_names, _ = process_run_args(remote_run_names) local_report_hashes = set([r.report_hash for r in report_dir_results]) if diff_type == ttypes.DiffType.NEW: @@ -665,17 +664,16 @@ def get_diff_local_dir_remote_run(client, report_dir, run_name): return filtered_reports, run_names - def get_diff_remote_run_local_dir(client, run_name, report_dir): + def get_diff_remote_run_local_dir(client, remote_run_names, report_dirs): """ Compares a remote run with a local report directory. """ filtered_reports = [] - report_dir_results = get_report_dir_results( - os.path.abspath(report_dir)) + report_dir_results = get_report_dir_results(report_dirs) suppressed_in_code = get_suppressed_reports(report_dir_results) diff_type = get_diff_type() - run_ids, run_names, _ = process_run_arg(run_name) + run_ids, run_names, _ = process_run_args(remote_run_names) local_report_hashes = set([r.report_hash for r in report_dir_results]) remote_hashes = client.getDiffResultsHash(run_ids, @@ -703,20 +701,23 @@ def get_diff_remote_run_local_dir(client, run_name, report_dir): return filtered_reports, run_names - def get_diff_remote_runs(client, basename, newname): + def get_diff_remote_runs(client, remote_base_run_names, + remote_new_run_names): """ Compares two remote runs and returns the filtered results. """ report_filter = ttypes.ReportFilter() add_filter_conditions(client, report_filter, args) - base_ids, base_run_names, base_run_tags = process_run_arg(basename) + base_ids, base_run_names, base_run_tags = \ + process_run_args(remote_base_run_names) report_filter.runTag = base_run_tags cmp_data = ttypes.CompareData() cmp_data.diffType = get_diff_type() - new_ids, new_run_names, new_run_tags = process_run_arg(newname) + new_ids, new_run_names, new_run_tags = \ + process_run_args(remote_new_run_names) cmp_data.runIds = new_ids cmp_data.runTag = new_run_tags @@ -751,13 +752,13 @@ def get_diff_remote_runs(client, basename, newname): return all_results, base_run_names, new_run_names - def get_diff_local_dirs(basename, newname): + def get_diff_local_dirs(base_run_names, new_run_names): """ Compares two report directories and returns the filtered results. """ filtered_reports = [] - base_results = get_report_dir_results(os.path.abspath(basename)) - new_results = get_report_dir_results(os.path.abspath(newname)) + base_results = get_report_dir_results(base_run_names) + new_results = get_report_dir_results(new_run_names) base_hashes = set([res.report_hash for res in base_results]) new_hashes = set([res.report_hash for res in new_results]) @@ -1081,33 +1082,39 @@ def get_run_tag(client, run_ids, tag_name): return run_histories[0] if run_histories else None - def process_run_arg(run_arg_with_tag): - """ - Process the argument and returns run ids a run tag ids. - The argument has the following format: : - """ - run_with_tag = run_arg_with_tag.split(':') - run_name = run_with_tag[0] - run_filter = ttypes.RunFilter(names=[run_name]) - - runs = get_run_data(client, run_filter) - run_ids = map(lambda run: run.runId, runs) - run_names = map(lambda run: run.name, runs) - - # Set base run tag if it is available. - run_tag_name = run_with_tag[1] if len(run_with_tag) > 1 else None - run_tags = None - if run_tag_name: - tag = get_run_tag(client, run_ids, run_tag_name) - run_tags = [tag.id] if tag else None - - if not run_ids: - LOG.warning( - "No run names match the given pattern: %s", run_arg_with_tag) - sys.exit(1) + def process_run_args(run_args_with_tag): + """ Process the argument and returns run ids and run tag ids. - LOG.info("Matching runs: %s", - ', '.join(map(lambda run: run.name, runs))) + The elemnts inside the given run_args_with_tag list has the following + format: : + """ + run_ids = [] + run_names = [] + run_tags = [] + + for run_arg_with_tag in run_args_with_tag: + run_with_tag = run_arg_with_tag.split(':') + run_name = run_with_tag[0] + run_filter = ttypes.RunFilter(names=[run_name]) + + runs = get_run_data(client, run_filter) + if not runs: + LOG.warning("No run names match the given pattern: %s", + run_arg_with_tag) + sys.exit(1) + + r_ids = map(lambda run: run.runId, runs) + run_ids.extend(r_ids) + + r_names = map(lambda run: run.name, runs) + run_names.extend(r_names) + + # Set base run tag if it is available. + run_tag_name = run_with_tag[1] if len(run_with_tag) > 1 else None + if run_tag_name: + tag = get_run_tag(client, r_ids, run_tag_name) + if tag: + run_tags.append(tag.id) return run_ids, run_names, run_tags @@ -1122,48 +1129,88 @@ def print_diff_results(reports): client = None + def preprocess_run_args(run_args): + """ Preprocess the given run argument. + Get a list of local directories and remote runs by processing the + given argument. + """ + local_dirs = [] + run_names = [] + + for r in run_args: + if os.path.isdir(r): + local_dirs.append(os.path.abspath(r)) + else: + run_names.append(r) + + return local_dirs, run_names + + basename_local_dirs, basename_run_names = \ + preprocess_run_args(args.base_run_names) + + newname_local_dirs, newname_run_names = \ + preprocess_run_args(args.new_run_names) + + has_different_run_args = False + if basename_local_dirs and basename_run_names: + LOG.error("All base run names must have the same type: local " + "directory (%s) or run names (%s).", + ', '.join(basename_local_dirs), + ', '.join(basename_run_names)) + has_different_run_args = True + + if newname_local_dirs and newname_run_names: + LOG.error("All new run names must have the same type: local " + "directory (%s) or run names (%s).", + ', '.join(newname_local_dirs), + ', '.join(newname_run_names)) + has_different_run_args = True + + if has_different_run_args: + sys.exit(1) + # We set up the client if we are not comparing two local report directory. - if not os.path.isdir(args.basename) or not os.path.isdir(args.newname): + if basename_run_names or newname_run_names: client = setup_client(args.product_url) - if os.path.isdir(args.basename) and os.path.isdir(args.newname): - reports = get_diff_local_dirs(args.basename, args.newname) + if basename_local_dirs and newname_local_dirs: + reports = get_diff_local_dirs(basename_local_dirs, newname_local_dirs) print_diff_results(reports) - LOG.info("Compared two local report directories %s and %s", - os.path.abspath(args.basename), - os.path.abspath(args.newname)) - elif os.path.isdir(args.newname): - reports, base_run_names = \ + LOG.info("Compared the following local report directories: %s and %s", + ', '.join(basename_local_dirs), + ', '.join(newname_local_dirs)) + elif newname_local_dirs: + reports, matching_base_run_names = \ get_diff_remote_run_local_dir(client, - args.basename, - os.path.abspath(args.newname)) + basename_run_names, + newname_local_dirs) print_diff_results(reports) LOG.info("Compared remote run(s) %s (matching: %s) and local report " - "directory %s", - args.basename, - ', '.join(base_run_names), - os.path.abspath(args.newname)) - elif os.path.isdir(args.basename): - reports, new_run_names = \ + "directory(s) %s", + ', '.join(basename_run_names), + ', '.join(matching_base_run_names), + ', '.join(newname_local_dirs)) + elif basename_local_dirs: + reports, matching_new_run_names = \ get_diff_local_dir_remote_run(client, - os.path.abspath(args.basename), - args.newname) + basename_local_dirs, + newname_run_names) print_diff_results(reports) - LOG.info("Compared local report directory %s and remote run(s) %s " + LOG.info("Compared local report directory(s) %s and remote run(s) %s " "(matching: %s).", - os.path.abspath(args.basename), - args.newname, - ', '.join(new_run_names)) + ', '.join(basename_local_dirs), + ', '.join(newname_run_names), + ', '.join(matching_new_run_names)) else: - reports, base_run_names, new_run_names = \ - get_diff_remote_runs(client, args.basename, args.newname) + reports, matching_base_run_names, matching_new_run_names = \ + get_diff_remote_runs(client, basename_run_names, newname_run_names) print_diff_results(reports) LOG.info("Compared multiple remote runs %s (matching: %s) and %s " "(matching: %s)", - args.basename, - ', '.join(base_run_names), - args.newname, - ', '.join(new_run_names)) + ', '.join(basename_run_names), + ', '.join(matching_base_run_names), + ', '.join(newname_run_names), + ', '.join(matching_new_run_names)) def handle_list_result_types(args): diff --git a/web/tests/functional/diff_local/test_diff_local.py b/web/tests/functional/diff_local/test_diff_local.py index 9e610e165a..f80ead78ed 100644 --- a/web/tests/functional/diff_local/test_diff_local.py +++ b/web/tests/functional/diff_local/test_diff_local.py @@ -164,3 +164,15 @@ def test_filter_severity_high_low_json(self): print(high_low_unresolved_results) # FIXME check json output for the returned severity levels. + + def test_multiple_dir(self): + """ Get unresolved reports from muliple local directories. """ + unresolved_diff_cmd = [self._codechecker_cmd, 'cmd', 'diff', + '-b', self.base_reports, self.new_reports, + '-n', self.new_reports, self.base_reports, + '--unresolved', '-o', 'json'] + print(unresolved_diff_cmd) + out_json = subprocess.check_output(unresolved_diff_cmd) + print(out_json) + unresolved_results = json.loads(out_json) + self.assertNotEqual(len(unresolved_results), 0) diff --git a/web/tests/functional/diff_local_remote/test_diff_local_remote.py b/web/tests/functional/diff_local_remote/test_diff_local_remote.py index a04af3e8b2..9f0dc18712 100644 --- a/web/tests/functional/diff_local_remote/test_diff_local_remote.py +++ b/web/tests/functional/diff_local_remote/test_diff_local_remote.py @@ -300,3 +300,39 @@ def test_local_compare_res_html_output_unresolved(self): continue self.assertIn(file_name, checked_files) + + def test_different_basename_types(self): + """ Test different basename types. + + Test that diff command will fail when remote run and local report + directory are given to the basename parameter. + """ + base_run_name = self._run_names[0] + + diff_cmd = [self._codechecker_cmd, "cmd", "diff", + "-b", base_run_name, self._local_reports, + "-n", self._local_reports, + "--unresolved", + "--url", self._url] + + with self.assertRaises(subprocess.CalledProcessError): + subprocess.check_output(diff_cmd, env=self._env, + cwd=os.environ['TEST_WORKSPACE']) + + def test_different_newname_types(self): + """ Test different newname types. + + Test that diff command will fail when remote run and local report + directory are given to the newname parameter. + """ + base_run_name = self._run_names[0] + + diff_cmd = [self._codechecker_cmd, "cmd", "diff", + "-b", base_run_name, + "-n", self._local_reports, base_run_name, + "--unresolved", + "--url", self._url] + + with self.assertRaises(subprocess.CalledProcessError): + subprocess.check_output(diff_cmd, env=self._env, + cwd=os.environ['TEST_WORKSPACE']) diff --git a/web/tests/functional/diff_remote/test_diff_remote.py b/web/tests/functional/diff_remote/test_diff_remote.py index af09357781..ed1e67419f 100644 --- a/web/tests/functional/diff_remote/test_diff_remote.py +++ b/web/tests/functional/diff_remote/test_diff_remote.py @@ -14,6 +14,7 @@ from __future__ import division from __future__ import absolute_import +import json import os import re import subprocess @@ -569,3 +570,22 @@ def get_run_tag_id(tag_name): cmp_data, False) self.assertEqual(len(diff_res), 26) + + def test_multiple_runs(self): + """ Count the unresolved results in multiple runs without filter. """ + base_run_name = self._test_runs[0].name + new_run_name = self._test_runs[1].name + + diff_cmd = [self._codechecker_cmd, "cmd", "diff", + "-b", base_run_name, new_run_name, + "-n", new_run_name, base_run_name, + "--unresolved", + "-o", "json", + "--url", self._url] + print(diff_cmd) + out_json = subprocess.check_output(diff_cmd, + env=self._env, + cwd=os.environ['TEST_WORKSPACE']) + + unresolved_results = json.loads(out_json) + self.assertNotEqual(len(unresolved_results), 0)