diff --git a/analyzer/codechecker_analyzer/analysis_manager.py b/analyzer/codechecker_analyzer/analysis_manager.py index 9de00df99e..0ace700563 100644 --- a/analyzer/codechecker_analyzer/analysis_manager.py +++ b/analyzer/codechecker_analyzer/analysis_manager.py @@ -31,8 +31,8 @@ from . import gcc_toolchain from .analyzers import analyzer_types -from .analyzers.config_handler import CheckerState from .analyzers.clangsa.analyzer import ClangSA +from .analyzers.config_handler import CheckerState LOG = get_logger('analyzer') @@ -305,31 +305,6 @@ def handle_reproducer(source_analyzer, rh, zip_file, actions_map): LOG.debug("ZIP file written at '%s'", zip_file) -def handle_failure( - source_analyzer, rh, zip_file, result_base, actions_map, skip_handlers, - rs_handler: ReviewStatusHandler -): - """ - If the analysis fails a debug zip is packed together which contains - build, analysis information and source files to be able to - reproduce the failed analysis. - """ - handle_reproducer(source_analyzer, rh, zip_file, actions_map) - - # In case of compiler errors the error message still needs to be collected - # from the standard output by this postprocess phase so we can present them - # as CodeChecker reports. - checks = source_analyzer.config_handler.checks() - state = checks.get('clang-diagnostic-error', (CheckerState.ENABLED, ''))[0] - if state == CheckerState.ENABLED: - rh.postprocess_result(skip_handlers, rs_handler) - - # Remove files that successfully analyzed earlier on. - plist_file = result_base + ".plist" - if os.path.exists(plist_file): - os.remove(plist_file) - - def setup_process_timeout(proc, timeout, failure_callback=None): """ @@ -554,20 +529,34 @@ def handle_analysis_result(success, zip_file=zip_file): necessary files are collected for debugging. These .zip files can also be generated by --generate-reproducer flag. """ - if generate_reproducer: - handle_reproducer(source_analyzer, rh, - os.path.join(reproducer_dir, zip_file), - actions_map) - if success: handle_success(rh, result_file, result_base, filter_handlers, rs_handler, capture_analysis_output, success_dir) - elif not generate_reproducer: - handle_failure(source_analyzer, rh, - os.path.join(failed_dir, zip_file), - result_base, actions_map, skip_handlers, - rs_handler) + else: + # Postprocess failed analyses to capture compiler errors + # in reports/statistics, unless clang-diagnostic-error + # checker is explicitly disabled. + checks = source_analyzer.config_handler.checks() + state = checks.get('clang-diagnostic-error', + (CheckerState.ENABLED, ''))[0] + if state != CheckerState.DISABLED: + rh.postprocess_result(skip_handlers, rs_handler) + + # Remove stale plist files from previous successful runs + plist_file = result_base + ".plist" + if os.path.exists(plist_file): + os.remove(plist_file) + + # Generate reproducer or failure zip + if generate_reproducer: + handle_reproducer(source_analyzer, rh, + os.path.join(reproducer_dir, zip_file), + actions_map) + elif not success: + handle_reproducer(source_analyzer, rh, + os.path.join(failed_dir, zip_file), + actions_map) if rh.analyzer_returncode == 0: handle_analysis_result(success=True) diff --git a/analyzer/tests/functional/analyze/test_analyze.py b/analyzer/tests/functional/analyze/test_analyze.py index 0ca34ceb92..b192df50c2 100644 --- a/analyzer/tests/functional/analyze/test_analyze.py +++ b/analyzer/tests/functional/analyze/test_analyze.py @@ -484,6 +484,62 @@ def test_reproducer(self): shutil.rmtree(reproducer_dir) + def test_reproducer_postprocesses_failures(self): + """ + Test that --generate-reproducer still postprocesses compiler errors. + This is a regression test for a bug where --generate-reproducer + prevented postprocessing, causing failed analyses to not appear in + statistics or parse output. + """ + build_json = os.path.join(self.test_workspace, "build.json") + reproducer_dir = os.path.join(self.report_dir, "reproducer") + source_file = os.path.join(self.test_dir, "failure.c") + + build_log = [{"directory": self.test_workspace, + "command": "gcc -c " + source_file, + "file": source_file}] + + with open(build_json, 'w', + encoding="utf-8", errors="ignore") as outfile: + json.dump(build_log, outfile) + + # Analyze with --generate-reproducer + analyze_cmd = [self._codechecker_cmd, "analyze", build_json, + "--analyzers", "clang-tidy", + "-o", self.report_dir, "--generate-reproducer"] + + process = subprocess.Popen( + analyze_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=self.test_dir, + encoding="utf-8", + errors="ignore") + out, _ = process.communicate() + + # Verify that the failed analysis appears in statistics + self.assertIn("Failed to analyze", out) + self.assertIn("failure.c", out) + + # Parse the reports and verify the file is counted + # Before the fix, this would show "0" processed files + parse_cmd = [self._codechecker_cmd, "parse", self.report_dir] + process = subprocess.Popen( + parse_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=self.test_dir, + encoding="utf-8", + errors="ignore") + out, _ = process.communicate() + + # The key bug: with --generate-reproducer, postprocessing was skipped + # so parse would show 0 processed files. After the fix, it should + # show 1 processed file. + self.assertIn("Number of processed analyzer result files | 1", out) + + shutil.rmtree(reproducer_dir) + def test_robustness_for_dependencygen_failure(self): """ Test if failure ZIP is created even if the dependency generator creates