This commit is for the benefit of GDB, but as the binutils-gdb repository shares the contrib/ directory with GCC, this commit must first be applied to GCC and then copied back to binutils-gdb.
When running GDB tests in parallel (make check -j$(nproc)), the consolidated gdb.sum and gdb.log files are produced by contrib/dg-extract-results.py, which merges per-test output files. If any single per-test output file is malformed (e.g., due to a DejaGnu EILSEQ crash, which is how I encountered this problem), the script aborts via self.fatal(). Because this script is invoked via a Makefile command using shell redirection, this causes the top-level output files to be left as empty, zero-byte files, discarding valid results from all other tests. Fix by making the script tolerant of unparseable input files. Wrap each file's parsing in a try/except block. When a file cannot be parsed, emit a warning to stderr and continue processing remaining files. This ensures that crashing tests do not destroy the consolidated output for the entire parallel build. Tested on Fedora 44 using the GCC testsuite (make check-gcc -j$(nproc)). The consolidated results are produced correctly with no regressions. This commit fixes this GDB bug: https://sourceware.org/bugzilla/show_bug.cgi?id=34147 contrib/ChangeLog: * dg-extract-results.py: Show warnings instead of erroring out when encountering an unparseable file. --- contrib/dg-extract-results.py | 44 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/contrib/dg-extract-results.py b/contrib/dg-extract-results.py index c7060753500..98b0f4989c9 100644 --- a/contrib/dg-extract-results.py +++ b/contrib/dg-extract-results.py @@ -34,6 +34,16 @@ if sys.version_info >= (3, 0): sys.stdout = io.TextIOWrapper (sys.stdout.buffer, errors = 'surrogateescape') +# Exception raised to skip a file that cannot be parsed. Used when +# a summary or log file is malformed (e.g. due to a DejaGnu EILSEQ +# crash). We will warn about the file and continue processing the +# rest. +class ParseError (Exception): + def __init__ (self, filename, message): + Exception.__init__ (self, filename + ': ' + message) + self.filename = filename + self.message = message + class Named: def __init__ (self, name): self.name = name @@ -205,7 +215,7 @@ class Prog: try: return int (value) except ValueError: - self.fatal (filename, 'expected an integer, got: ' + value) + raise ParseError (filename, 'expected an integer, got: ' + value) # Return a list that represents no test results. def zero_counts (self): @@ -229,7 +239,7 @@ class Prog: while True: line = file.readline() if line == '': - self.fatal (filename, 'could not parse variation list') + raise ParseError (filename, 'could not parse variation list') if line == '\n': break self.known_variations.add (line.strip()) @@ -264,7 +274,7 @@ class Prog: while True: line = file.readline() if line == '': - self.fatal (filename, 'no recognised summary line') + raise ParseError (filename, 'no recognised summary line') if line == end: break @@ -292,7 +302,7 @@ class Prog: match = self.result_re.match (line) if match and (harness or not line.startswith ('WARNING:')): if not harness: - self.fatal (filename, 'saw test result before harness name') + raise ParseError (filename, 'saw test result before harness name') name = match.group (2) # Ugly hack to get the right order for gfortran. if name.startswith ('gfortran.dg/g77/'): @@ -354,7 +364,7 @@ class Prog: found = True break if not found: - self.fatal (filename, 'unknown test result: ' + line[:-1]) + raise ParseError (filename, 'unknown test result: ' + line[:-1]) # Parse an acats run, which uses a different format from dejagnu. # We have just skipped over '=== acats configuration ==='. @@ -367,7 +377,7 @@ class Prog: while True: line = file.readline() if line == '': - self.fatal (filename, 'could not parse acats preamble') + raise ParseError (filename, 'could not parse acats preamble') if line == '\t\t=== acats tests ===\n': break if record: @@ -423,9 +433,9 @@ class Prog: if line.startswith ('Running target '): name = line[len ('Running target '):-1] if not tool: - self.fatal (filename, 'could not parse tool name') + raise ParseError (filename, 'could not parse tool name') if name not in self.known_variations: - self.fatal (filename, 'unknown target: ' + name) + raise ParseError (filename, 'unknown target: ' + name) self.parse_run (filename, file, tool, tool.get_variation (name), num_variations) @@ -474,7 +484,7 @@ class Prog: # individual runs) and parse the version output. if tool and line == '\t\t=== ' + tool.name + ' Summary ===\n': if file.readline() != '\n': - self.fatal (filename, 'expected blank line after summary') + raise ParseError (filename, 'expected blank line after summary') self.parse_final_summary (filename, file) continue @@ -490,7 +500,7 @@ class Prog: # Sanity check to make sure that important text doesn't get # dropped accidentally. if strict and line.strip() != '': - self.fatal (filename, 'unrecognised line: ' + line[:-1]) + raise ParseError (filename, 'unrecognised line: ' + line[:-1]) # Output a segment of text. def output_segment (self, segment): @@ -569,8 +579,18 @@ class Prog: try: # Parse the input files. for filename in self.files: - with safe_open (filename) as file: - self.parse_file (filename, file) + try: + with safe_open (filename) as file: + self.parse_file (filename, file) + except ParseError as e: + # Partial state from this file is intentionally retained. + # This preserves any valid results and diagnostic ERROR + # lines that were parsed before the error, which is + # important for diagnosing problems like DejaGnu crashes. + # The unprocessed remainder of the file is lost. + sys.stderr.write ('warning: skipping ' + e.filename + ': ' + + e.message + + '; results may be incomplete\n') # Decide what to output. if len (self.variations) == 0: -- 2.54.0
