diff --git a/web/client/codechecker_client/cli/cmd.py b/web/client/codechecker_client/cli/cmd.py index 6be5de36b6..eb89177623 100644 --- a/web/client/codechecker_client/cli/cmd.py +++ b/web/client/codechecker_client/cli/cmd.py @@ -1079,14 +1079,6 @@ def __register_runs(parser): "run_2_c_name, run_3_d_name then \"run_2* " "run_3_d_name\" shows the last three runs.") - group.add_argument('--details', - default=argparse.SUPPRESS, - action='store_true', - required=False, - help="Adds extra details to the run information in " - "JSON format, such as the list of files that are " - "failed to analyze.") - group.add_argument('--all-before-run', type=str, dest="all_before_run", @@ -1298,6 +1290,7 @@ def add_arguments_to_parser(parser): formatter_class=argparse.ArgumentDefaultsHelpFormatter, description="List the analysis runs available on the server.", help="List the available analysis runs.") + __register_runs(runs) runs.set_defaults(func=cmd_line_client.handle_list_runs) __add_common_arguments(runs, output_formats=DEFAULT_OUTPUT_FORMATS) diff --git a/web/client/codechecker_client/cmd_line_client.py b/web/client/codechecker_client/cmd_line_client.py index 7b526e0662..6a9db68ab3 100644 --- a/web/client/codechecker_client/cmd_line_client.py +++ b/web/client/codechecker_client/cmd_line_client.py @@ -19,6 +19,7 @@ import sys import shutil import time +import json from typing import Dict, Iterable, List, Optional, Set, Tuple from codechecker_api.codeCheckerDBAccess_v6 import constants, ttypes @@ -645,11 +646,25 @@ def handle_list_runs(args): # to a json format. results = [] for run in runs: - if 'details' in args and args.details: - run.analyzerStatistics = \ - client.getAnalysisStatistics(run.runId, None) + enabled_checkers: Dict[str, Set[str]] = {} + info_list: List[ttypes.AnalysisInfo] = client.getAnalysisInfo( + ttypes.AnalysisInfoFilter(runId=run.runId), + constants.MAX_QUERY_SIZE, + 0) + for info in info_list: + for analyzer, checkers in (info.checkers or {}).items(): + enabled_checkers.setdefault(analyzer, set()) + for checker_name, checker_info in (checkers or {}).items(): + if checker_info.enabled: + enabled_checkers[analyzer].add(checker_name) + run.analyzerStatistics = client.getAnalysisStatistics( + run.runId, None) or {} + for analyzer in run.analyzerStatistics: + stat = run.analyzerStatistics[analyzer] + stat.enabledCheckers = sorted( + list(enabled_checkers.get(analyzer, set()))) results.append({run.name: run}) - print(CmdLineOutputEncoder().encode(results)) + print(json.dumps(results, cls=CmdLineOutputEncoder, indent=4)) else: # plaintext, csv header = ['Name', 'Number of unresolved reports', @@ -723,11 +738,9 @@ def handle_list_results(args): query_report_details) if args.output_format == 'json': - if 'details' in args: - run_id2name = {run.runId: run.name for run in run_data} - for report in all_results: - report.runName = run_id2name[report.runId] - + run_id2name = {run.runId: run.name for run in run_data} + for report in all_results: + report.runName = run_id2name[report.runId] print(CmdLineOutputEncoder().encode(all_results)) else: header = ['File', 'Checker', 'Severity', 'Message', 'Bug path length', diff --git a/web/client/codechecker_client/helpers/results.py b/web/client/codechecker_client/helpers/results.py index c558cfe040..ee4b1ee0b6 100644 --- a/web/client/codechecker_client/helpers/results.py +++ b/web/client/codechecker_client/helpers/results.py @@ -196,3 +196,7 @@ def getAnalysisStatistics(self, run_id, run_history_id): @thrift_client_call def storeAnalysisStatistics(self, run_name, zip_file): pass + + @thrift_client_call + def getAnalysisInfo(self, analysis_info_filter, limit, offset): + pass diff --git a/web/client/tests/unit/test_cmd_line_client.py b/web/client/tests/unit/test_cmd_line_client.py new file mode 100644 index 0000000000..5be6215a94 --- /dev/null +++ b/web/client/tests/unit/test_cmd_line_client.py @@ -0,0 +1,85 @@ +import io +import json +import unittest +from types import SimpleNamespace +from contextlib import redirect_stdout +from unittest.mock import Mock, patch + +from codechecker_client import cmd_line_client + + +class Args(SimpleNamespace): + def __contains__(self, key): + return hasattr(self, key) + + +class DummyRun: + def __init__(self, runId, name="r"): + self.runId = runId + self.name = name + self.resultCount = 0 + self.analyzerStatistics = {} + self.runDate = "" + self.versionTag = "" + self.duration = 0 + self.description = "" + self.codeCheckerVersion = "" + + +class DummyCheckerCfg: + def __init__(self, enabled): + self.enabled = enabled + + +def make_analysis_info(checkers_dict): + checkers = {} + for analyzer, ckrs in checkers_dict.items(): + checkers[analyzer] = {name: DummyCheckerCfg(en) for name, en in ckrs.items()} + return SimpleNamespace(checkers=checkers) + + +class ListRunsEnabledCheckersTest(unittest.TestCase): + def setUp(self): + cmd_line_client.LOG = SimpleNamespace(error=Mock(), warning=Mock(), info=Mock()) + + @patch("codechecker_client.cmd_line_client.init_logger") + @patch("codechecker_client.cmd_line_client.setup_client") + @patch("codechecker_client.cmd_line_client.get_run_data") + def test_enabled_checkers_json(self, get_run_data, setup_client, init_logger): + client = Mock() + setup_client.return_value = client + get_run_data.return_value = [DummyRun(1)] + + client.getAnalysisInfo.return_value = [ + make_analysis_info({"clangsa": {"a": True}}) + ] + client.getAnalysisStatistics.return_value = { + "clangsa": SimpleNamespace( + version="1", + failed=0, + successful=1, + failedFilePaths=[], + ) + } + + args = Args( + product_url="dummy", + sort_type="name", + sort_order="asc", + output_format="json" + ) + + buf = io.StringIO() + with redirect_stdout(buf): + cmd_line_client.handle_list_runs(args) + + data = json.loads(buf.getvalue()) + self.assertEqual(len(data), 1) + run_entry = data[0] + self.assertEqual(len(run_entry), 1) + _, run_data = next(iter(run_entry.items())) + self.assertIn("analyzerStatistics", run_data) + self.assertIn("clangsa", run_data["analyzerStatistics"]) + stats = run_data["analyzerStatistics"]["clangsa"] + self.assertIn("enabledCheckers", stats) + self.assertEqual(stats["enabledCheckers"], ["a"]) diff --git a/web/tests/functional/cmdline/test_cmdline.py b/web/tests/functional/cmdline/test_cmdline.py index ae654109a4..a174812297 100644 --- a/web/tests/functional/cmdline/test_cmdline.py +++ b/web/tests/functional/cmdline/test_cmdline.py @@ -230,23 +230,18 @@ def test_runs_analysis_statistics(self): '-o', 'json', '--url', str(self.server_url)] ret, res, _ = run_cmd(res_cmd, environ=environ) - self.assertEqual(0, ret) - for run in json.loads(res): - for data in run.values(): - self.assertIsNone( - data['analyzerStatistics']['clangsa']['failedFilePaths']) - - res_cmd = [self._codechecker_cmd, 'cmd', 'runs', - '-o', 'json', '--url', str(self.server_url), - '--details'] - ret, res, _ = run_cmd(res_cmd, environ=environ) - self.assertEqual(0, ret) for run in json.loads(res): for data in run.values(): self.assertEqual( data['analyzerStatistics']['clangsa']['failedFilePaths'], []) + self.assertNotEqual( + data['analyzerStatistics']['clangsa']['enabledCheckers'], + []) + self.assertIn( + 'core.NullDereference', + data['analyzerStatistics']['clangsa']['enabledCheckers']) def test_proxy_settings(self): """ Test proxy settings validation. """ @@ -352,8 +347,7 @@ def test_detailed_results_contain_run_names(self): check_env = self._test_config['codechecker_cfg']['check_env'] res_cmd = [self._codechecker_cmd, 'cmd', 'results', 'test_files1*', - 'test_files1*', '-o', 'json', '--url', str(self.server_url), - '--details'] + 'test_files1*', '-o', 'json', '--url', str(self.server_url)] ret, out, _ = run_cmd(res_cmd, environ=check_env) self.assertEqual(0, ret)