@@ -295,17 +295,17 @@ def parse_autest_output(output: str) -> dict:
295295 result ['skipped' ] = int (line .split (':' )[- 1 ].strip ())
296296 except ValueError :
297297 pass
298- elif 'Warning:' in line :
298+ elif re . match ( r'warnings?:' , line , re . IGNORECASE ) :
299299 try :
300300 result ['warnings' ] = int (line .split (':' )[- 1 ].strip ())
301301 except ValueError :
302302 pass
303- elif 'Exception:' in line :
303+ elif re . match ( r'exceptions?:' , line , re . IGNORECASE ) :
304304 try :
305305 result ['exceptions' ] = int (line .split (':' )[- 1 ].strip ())
306306 except ValueError :
307307 pass
308- elif 'Unknown:' in line :
308+ elif re . match ( r'unknowns?:' , line , re . IGNORECASE ) :
309309 try :
310310 result ['unknown' ] = int (line .split (':' )[- 1 ].strip ())
311311 except ValueError :
@@ -367,6 +367,60 @@ def extract_failure_output(output: str, failed_tests: List[str]) -> Dict[str, st
367367 return details
368368
369369
370+ def extract_worker_diagnostics (output : str ) -> str :
371+ """
372+ Extract the most relevant worker diagnostics for exception/setup failures.
373+
374+ When autest reports an exception without attributing it to a specific
375+ failed test, the best fallback is the worker's captured output. This
376+ helper prefers traceback/error sections but falls back to the full worker
377+ output if no obvious diagnostic block is found.
378+ """
379+ clean = strip_ansi (output ).strip ()
380+ if not clean :
381+ return ""
382+
383+ lines = clean .split ('\n ' )
384+ ranges : List [Tuple [int , int ]] = []
385+
386+ def is_marker (line : str ) -> bool :
387+ lower = line .lower ()
388+ return (
389+ 'traceback (most recent call last):' in lower or 'error:' in lower or 'timeout' in lower or
390+ 'interrupted' in lower
391+ )
392+
393+ for i , line in enumerate (lines ):
394+ if not is_marker (line ):
395+ continue
396+ start = max (0 , i - 5 )
397+ end = i + 1
398+ while end < len (lines ):
399+ candidate = lines [end ].strip ()
400+ if not candidate and end > i + 1 :
401+ break
402+ end += 1
403+ ranges .append ((start , min (end , len (lines ))))
404+
405+ if not ranges :
406+ return clean
407+
408+ merged_ranges : List [Tuple [int , int ]] = []
409+ for start , end in ranges :
410+ if merged_ranges and start <= merged_ranges [- 1 ][1 ]:
411+ merged_ranges [- 1 ] = (merged_ranges [- 1 ][0 ], max (merged_ranges [- 1 ][1 ], end ))
412+ else :
413+ merged_ranges .append ((start , end ))
414+
415+ blocks = []
416+ for start , end in merged_ranges :
417+ block = '\n ' .join (lines [start :end ]).rstrip ()
418+ if block :
419+ blocks .append (block )
420+
421+ return '\n \n ...\n \n ' .join (blocks ) if blocks else clean
422+
423+
370424def run_single_test (test : str , script_dir : Path , sandbox : Path , ats_bin : str , build_root : str , extra_args : List [str ],
371425 env : dict ) -> Tuple [str , float , str , str ]:
372426 """
@@ -672,6 +726,20 @@ def print_summary(results: List[TestResult], total_duration: float, expected_tim
672726 print (details [test_name ])
673727 print ("-" * 70 )
674728
729+ worker_diagnostics = [
730+ r for r in results
731+ if r .output and (r .exceptions > 0 or (r .return_code != 0 and not r .failed_tests ))
732+ ]
733+ if worker_diagnostics :
734+ print ("-" * 70 )
735+ print ("WORKER DIAGNOSTICS:" )
736+ print ("-" * 70 )
737+ for r in worker_diagnostics :
738+ label = "Serial" if r .is_serial else f"Worker { r .worker_id } "
739+ print (f"\n --- { label } (return code { r .return_code } ) ---" )
740+ print (extract_worker_diagnostics (r .output ))
741+ print ("-" * 70 )
742+
675743 # Check for timing discrepancies
676744 if expected_timings and actual_timings :
677745 timing_warnings = []
0 commit comments