From 0ff6e341fd82dc94e00011fd4b47ab524dce4294 Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Wed, 11 Feb 2026 20:57:43 +0000 Subject: [PATCH] parallel autests: print failure output The parallel runner captured full autest output per worker but only displayed failed test names in the summary. This made CI failures hard to debug compared to the sequential runner. Extract and print the per-test failure detail block (sub-step pass/fail, reasons, file paths) so the same diagnostic information is available in parallel mode. --- tests/autest-parallel.py.in | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/autest-parallel.py.in b/tests/autest-parallel.py.in index a79fd080327..be5f5d9129e 100755 --- a/tests/autest-parallel.py.in +++ b/tests/autest-parallel.py.in @@ -319,6 +319,54 @@ def parse_autest_output(output: str) -> dict: return result +def extract_failure_output(output: str, failed_tests: List[str]) -> Dict[str, str]: + """ + Extract the detailed autest output section for each failed test. + + Autest emits a block per test starting with a line like + ``Test: : Failed`` and continuing with indented detail lines until + the next top-level ``Test:`` line or the end of meaningful output. + + Args: + output: Raw autest output from a worker + failed_tests: List of test names that failed + + Returns: + Dictionary mapping each failed test name to its detail block + """ + if not failed_tests: + return {} + + clean = strip_ansi(output) + lines = clean.split('\n') + + # Identify line ranges for each top-level "Test: :" block. + test_block_re = re.compile(r'^Test:\s+(\S+):\s+(Passed|Failed|Skipped)', re.IGNORECASE) + block_starts: List[Tuple[int, str]] = [] + for i, line in enumerate(lines): + m = test_block_re.match(line.strip()) + if m: + block_starts.append((i, m.group(1))) + + failed_set = set(failed_tests) + details: Dict[str, str] = {} + + for idx, (start_line, name) in enumerate(block_starts): + if name not in failed_set: + continue + # The block extends until the next top-level Test: line or end of + # output. + if idx + 1 < len(block_starts): + end_line = block_starts[idx + 1][0] + else: + end_line = len(lines) + block = '\n'.join(lines[start_line:end_line]).rstrip() + if block: + details[name] = block + + return details + + def run_single_test(test: str, script_dir: Path, sandbox: Path, ats_bin: str, build_root: str, extra_args: List[str], env: dict) -> Tuple[str, float, str, str]: """ @@ -610,6 +658,20 @@ def print_summary(results: List[TestResult], total_duration: float, expected_tim for test in sorted(all_failed_tests): print(f" - {test}") + # Print detailed failure output extracted from each worker's autest + # output so CI logs contain actionable diagnostics. + print("-" * 70) + print("FAILED TEST OUTPUT:") + print("-" * 70) + for r in results: + if not r.failed_tests or not r.output: + continue + details = extract_failure_output(r.output, r.failed_tests) + for test_name in sorted(details): + print(f"\n--- {test_name} ---") + print(details[test_name]) + print("-" * 70) + # Check for timing discrepancies if expected_timings and actual_timings: timing_warnings = []