Home | History | Annotate | Line # | Download | only in contrib
dg-extract-results.py revision 1.1.1.4
      1 #!/usr/bin/python
      2 #
      3 # Copyright (C) 2014-2025 Free Software Foundation, Inc.
      4 #
      5 # This script is free software; you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as published by
      7 # the Free Software Foundation; either version 3, or (at your option)
      8 # any later version.
      9 
     10 import sys
     11 import getopt
     12 import re
     13 import io
     14 from datetime import datetime
     15 from operator import attrgetter
     16 
     17 # True if unrecognised lines should cause a fatal error.  Might want to turn
     18 # this on by default later.
     19 strict = False
     20 
     21 # True if the order of .log segments should match the .sum file, false if
     22 # they should keep the original order.
     23 sort_logs = True
     24 
     25 # A version of open() that is safe against whatever binary output
     26 # might be added to the log.
     27 def safe_open (filename):
     28     if sys.version_info >= (3, 0):
     29         return open (filename, 'r', errors = 'surrogateescape')
     30     return open (filename, 'r')
     31 
     32 # Force stdout to handle escape sequences from a safe_open file.
     33 if sys.version_info >= (3, 0):
     34     sys.stdout = io.TextIOWrapper (sys.stdout.buffer,
     35                                    errors = 'surrogateescape')
     36 
     37 class Named:
     38     def __init__ (self, name):
     39         self.name = name
     40 
     41 class ToolRun (Named):
     42     def __init__ (self, name):
     43         Named.__init__ (self, name)
     44         # The variations run for this tool, mapped by --target_board name.
     45         self.variations = dict()
     46 
     47     # Return the VariationRun for variation NAME.
     48     def get_variation (self, name):
     49         if name not in self.variations:
     50             self.variations[name] = VariationRun (name)
     51         return self.variations[name]
     52 
     53 class VariationRun (Named):
     54     def __init__ (self, name):
     55         Named.__init__ (self, name)
     56         # A segment of text before the harness runs start, describing which
     57         # baseboard files were loaded for the target.
     58         self.header = None
     59         # The harnesses run for this variation, mapped by filename.
     60         self.harnesses = dict()
     61         # A list giving the number of times each type of result has
     62         # been seen.
     63         self.counts = []
     64 
     65     # Return the HarnessRun for harness NAME.
     66     def get_harness (self, name):
     67         if name not in self.harnesses:
     68             self.harnesses[name] = HarnessRun (name)
     69         return self.harnesses[name]
     70 
     71 class HarnessRun (Named):
     72     def __init__ (self, name):
     73         Named.__init__ (self, name)
     74         # Segments of text that make up the harness run, mapped by a test-based
     75         # key that can be used to order them.
     76         self.segments = dict()
     77         # Segments of text that make up the harness run but which have
     78         # no recognized test results.  These are typically harnesses that
     79         # are completely skipped for the target.
     80         self.empty = []
     81         # A list of results.  Each entry is a pair in which the first element
     82         # is a unique sorting key and in which the second is the full
     83         # PASS/FAIL line.
     84         self.results = []
     85 
     86     # Add a segment of text to the harness run.  If the segment includes
     87     # test results, KEY is an example of one of them, and can be used to
     88     # combine the individual segments in order.  If the segment has no
     89     # test results (e.g. because the harness doesn't do anything for the
     90     # current configuration) then KEY is None instead.  In that case
     91     # just collect the segments in the order that we see them.
     92     def add_segment (self, key, segment):
     93         if key:
     94             assert key not in self.segments
     95             self.segments[key] = segment
     96         else:
     97             self.empty.append (segment)
     98 
     99 class Segment:
    100     def __init__ (self, filename, start):
    101         self.filename = filename
    102         self.start = start
    103         self.lines = 0
    104 
    105 class Prog:
    106     def __init__ (self):
    107         # The variations specified on the command line.
    108         self.variations = []
    109         # The variations seen in the input files.
    110         self.known_variations = set()
    111         # The tools specified on the command line.
    112         self.tools = []
    113         # Whether to create .sum rather than .log output.
    114         self.do_sum = True
    115         # Regexps used while parsing.
    116         self.test_run_re = re.compile (r'^Test run by (\S+) on (.*)$',
    117                                        re.IGNORECASE)
    118         self.tool_re = re.compile (r'^\t\t=== (.*) tests ===$')
    119         self.result_re = re.compile (r'^(PASS|XPASS|FAIL|XFAIL|UNRESOLVED'
    120                                      r'|WARNING|ERROR|UNSUPPORTED|UNTESTED'
    121                                      r'|KFAIL|KPASS|PATH|DUPLICATE):\s*(.+)')
    122         self.completed_re = re.compile (r'.* completed at (.*)')
    123         # Pieces of text to write at the head of the output.
    124         # start_line is a pair in which the first element is a datetime
    125         # and in which the second is the associated 'Test Run By' line.
    126         self.start_line = None
    127         self.native_line = ''
    128         self.target_line = ''
    129         self.host_line = ''
    130         self.acats_premable = ''
    131         # Pieces of text to write at the end of the output.
    132         # end_line is like start_line but for the 'runtest completed' line.
    133         self.acats_failures = []
    134         self.version_output = ''
    135         self.end_line = None
    136         # Known summary types.
    137         self.count_names = [
    138             '# of DejaGnu errors\t\t',
    139             '# of expected passes\t\t',
    140             '# of unexpected failures\t',
    141             '# of unexpected successes\t',
    142             '# of expected failures\t\t',
    143             '# of unknown successes\t\t',
    144             '# of known failures\t\t',
    145             '# of untested testcases\t\t',
    146             '# of unresolved testcases\t',
    147             '# of unsupported tests\t\t',
    148             '# of paths in test names\t',
    149             '# of duplicate test names\t',
    150             '# of unexpected core files\t'
    151         ]
    152         self.runs = dict()
    153 
    154     def usage (self):
    155         name = sys.argv[0]
    156         sys.stderr.write ('Usage: ' + name
    157                           + ''' [-t tool] [-l variant-list] [-L] log-or-sum-file ...
    158 
    159     tool           The tool (e.g. g++, libffi) for which to create a
    160                    new test summary file.  If not specified then output
    161                    is created for all tools.
    162     variant-list   One or more test variant names.  If the list is
    163                    not specified then one is constructed from all
    164                    variants in the files for <tool>.
    165     sum-file       A test summary file with the format of those
    166                    created by runtest from DejaGnu.
    167     If -L is used, merge *.log files instead of *.sum.  In this
    168     mode the exact order of lines may not be preserved, just different
    169     Running *.exp chunks should be in correct order.
    170 ''')
    171         sys.exit (1)
    172 
    173     def fatal (self, what, string):
    174         if not what:
    175             what = sys.argv[0]
    176         sys.stderr.write (what + ': ' + string + '\n')
    177         sys.exit (1)
    178 
    179     # Parse the command-line arguments.
    180     def parse_cmdline (self):
    181         try:
    182             (options, self.files) = getopt.getopt (sys.argv[1:], 'l:t:L')
    183             if len (self.files) == 0:
    184                 self.usage()
    185             for (option, value) in options:
    186                 if option == '-l':
    187                     self.variations.append (value)
    188                 elif option == '-t':
    189                     self.tools.append (value)
    190                 else:
    191                     self.do_sum = False
    192         except getopt.GetoptError as e:
    193             self.fatal (None, e.msg)
    194 
    195     # Try to parse time string TIME, returning an arbitrary time on failure.
    196     # Getting this right is just a nice-to-have so failures should be silent.
    197     def parse_time (self, time):
    198         try:
    199             return datetime.strptime (time, '%c')
    200         except ValueError:
    201             return datetime.now()
    202 
    203     # Parse an integer and abort on failure.
    204     def parse_int (self, filename, value):
    205         try:
    206             return int (value)
    207         except ValueError:
    208             self.fatal (filename, 'expected an integer, got: ' + value)
    209 
    210     # Return a list that represents no test results.
    211     def zero_counts (self):
    212         return [0 for x in self.count_names]
    213 
    214     # Return the ToolRun for tool NAME.
    215     def get_tool (self, name):
    216         if name not in self.runs:
    217             self.runs[name] = ToolRun (name)
    218         return self.runs[name]
    219 
    220     # Add the result counts in list FROMC to TOC.
    221     def accumulate_counts (self, toc, fromc):
    222         for i in range (len (self.count_names)):
    223             toc[i] += fromc[i]
    224 
    225     # Parse the list of variations after 'Schedule of variations:'.
    226     # Return the number seen.
    227     def parse_variations (self, filename, file):
    228         num_variations = 0
    229         while True:
    230             line = file.readline()
    231             if line == '':
    232                 self.fatal (filename, 'could not parse variation list')
    233             if line == '\n':
    234                 break
    235             self.known_variations.add (line.strip())
    236             num_variations += 1
    237         return num_variations
    238 
    239     # Parse from the first line after 'Running target ...' to the end
    240     # of the run's summary.
    241     def parse_run (self, filename, file, tool, variation, num_variations):
    242         header = None
    243         harness = None
    244         segment = None
    245         final_using = 0
    246         has_warning = 0
    247 
    248         # If this is the first run for this variation, add any text before
    249         # the first harness to the header.
    250         if not variation.header:
    251             segment = Segment (filename, file.tell())
    252             variation.header = segment
    253 
    254         # Parse the rest of the summary (the '# of ' lines).
    255         if len (variation.counts) == 0:
    256             variation.counts = self.zero_counts()
    257 
    258         # Parse up until the first line of the summary.
    259         if num_variations == 1:
    260             end = '\t\t=== ' + tool.name + ' Summary ===\n'
    261         else:
    262             end = ('\t\t=== ' + tool.name + ' Summary for '
    263                    + variation.name + ' ===\n')
    264         while True:
    265             line = file.readline()
    266             if line == '':
    267                 self.fatal (filename, 'no recognised summary line')
    268             if line == end:
    269                 break
    270 
    271             # Look for the start of a new harness.
    272             if line.startswith ('Running ') and line.endswith (' ...\n'):
    273                 # Close off the current harness segment, if any.
    274                 if harness:
    275                     segment.lines -= final_using
    276                     harness.add_segment (first_key, segment)
    277                 name = line[len ('Running '):-len(' ...\n')]
    278                 harness = variation.get_harness (name)
    279                 segment = Segment (filename, file.tell())
    280                 first_key = None
    281                 final_using = 0
    282                 continue
    283 
    284             # Record test results.  Associate the first test result with
    285             # the harness segment, so that if a run for a particular harness
    286             # has been split up, we can reassemble the individual segments
    287             # in a sensible order.
    288             #
    289             # dejagnu sometimes issues warnings about the testing environment
    290             # before running any tests.  Treat them as part of the header
    291             # rather than as a test result.
    292             match = self.result_re.match (line)
    293             if match and (harness or not line.startswith ('WARNING:')):
    294                 if not harness:
    295                     self.fatal (filename, 'saw test result before harness name')
    296                 name = match.group (2)
    297                 # Ugly hack to get the right order for gfortran.
    298                 if name.startswith ('gfortran.dg/g77/'):
    299                     name = 'h' + name
    300                 # If we have a time out warning, make sure it appears
    301                 # before the following testcase diagnostic: we insert
    302                 # the testname before 'program' so that sort faces a
    303                 # list of testnames.
    304                 if line.startswith ('WARNING: program timed out'):
    305                   has_warning = 1
    306                 else:
    307                   if has_warning == 1:
    308                       key = (name, len (harness.results))
    309                       myline = 'WARNING: %s program timed out.\n' % name
    310                       harness.results.append ((key, myline))
    311                       has_warning = 0
    312                   key = (name, len (harness.results))
    313                   harness.results.append ((key, line))
    314                   if not first_key and sort_logs:
    315                       first_key = key
    316                 if line.startswith ('ERROR: (DejaGnu)'):
    317                     for i in range (len (self.count_names)):
    318                         if 'DejaGnu errors' in self.count_names[i]:
    319                             variation.counts[i] += 1
    320                             break
    321 
    322             # 'Using ...' lines are only interesting in a header.  Splitting
    323             # the test up into parallel runs leads to more 'Using ...' lines
    324             # than there would be in a single log.
    325             if line.startswith ('Using '):
    326                 final_using += 1
    327             else:
    328                 final_using = 0
    329 
    330             # Add other text to the current segment, if any.
    331             if segment:
    332                 segment.lines += 1
    333 
    334         # Close off the final harness segment, if any.
    335         if harness:
    336             segment.lines -= final_using
    337             harness.add_segment (first_key, segment)
    338 
    339         while True:
    340             before = file.tell()
    341             line = file.readline()
    342             if line == '':
    343                 break
    344             if line == '\n':
    345                 continue
    346             if not line.startswith ('# '):
    347                 file.seek (before)
    348                 break
    349             found = False
    350             for i in range (len (self.count_names)):
    351                 if line.startswith (self.count_names[i]):
    352                     count = line[len (self.count_names[i]):-1].strip()
    353                     variation.counts[i] += self.parse_int (filename, count)
    354                     found = True
    355                     break
    356             if not found:
    357                 self.fatal (filename, 'unknown test result: ' + line[:-1])
    358 
    359     # Parse an acats run, which uses a different format from dejagnu.
    360     # We have just skipped over '=== acats configuration ==='.
    361     def parse_acats_run (self, filename, file):
    362         # Parse the preamble, which describes the configuration and logs
    363         # the creation of support files.
    364         record = (self.acats_premable == '')
    365         if record:
    366             self.acats_premable = '\t\t=== acats configuration ===\n'
    367         while True:
    368             line = file.readline()
    369             if line == '':
    370                 self.fatal (filename, 'could not parse acats preamble')
    371             if line == '\t\t=== acats tests ===\n':
    372                 break
    373             if record:
    374                 self.acats_premable += line
    375 
    376         # Parse the test results themselves, using a dummy variation name.
    377         tool = self.get_tool ('acats')
    378         variation = tool.get_variation ('none')
    379         self.parse_run (filename, file, tool, variation, 1)
    380 
    381         # Parse the failure list.
    382         while True:
    383             before = file.tell()
    384             line = file.readline()
    385             if line.startswith ('*** FAILURES: '):
    386                 self.acats_failures.append (line[len ('*** FAILURES: '):-1])
    387                 continue
    388             file.seek (before)
    389             break
    390 
    391     # Parse the final summary at the end of a log in order to capture
    392     # the version output that follows it.
    393     def parse_final_summary (self, filename, file):
    394         record = (self.version_output == '')
    395         while True:
    396             line = file.readline()
    397             if line == '':
    398                 break
    399             if line.startswith ('# of '):
    400                 continue
    401             if record:
    402                 self.version_output += line
    403             if line == '\n':
    404                 break
    405 
    406     # Parse a .log or .sum file.
    407     def parse_file (self, filename, file):
    408         tool = None
    409         target = None
    410         num_variations = 1
    411         while True:
    412             line = file.readline()
    413             if line == '':
    414                 return
    415 
    416             # Parse the list of variations, which comes before the test
    417             # runs themselves.
    418             if line.startswith ('Schedule of variations:'):
    419                 num_variations = self.parse_variations (filename, file)
    420                 continue
    421 
    422             # Parse a testsuite run for one tool/variation combination.
    423             if line.startswith ('Running target '):
    424                 name = line[len ('Running target '):-1]
    425                 if not tool:
    426                     self.fatal (filename, 'could not parse tool name')
    427                 if name not in self.known_variations:
    428                     self.fatal (filename, 'unknown target: ' + name)
    429                 self.parse_run (filename, file, tool,
    430                                 tool.get_variation (name),
    431                                 num_variations)
    432                 # If there is only one variation then there is no separate
    433                 # summary for it.  Record any following version output.
    434                 if num_variations == 1:
    435                     self.parse_final_summary (filename, file)
    436                 continue
    437 
    438             # Parse the start line.  In the case where several files are being
    439             # parsed, pick the one with the earliest time.
    440             match = self.test_run_re.match (line)
    441             if match:
    442                 time = self.parse_time (match.group (2))
    443                 if not self.start_line or self.start_line[0] > time:
    444                     self.start_line = (time, line)
    445                 continue
    446 
    447             # Parse the form used for native testing.
    448             if line.startswith ('Native configuration is '):
    449                 self.native_line = line
    450                 continue
    451 
    452             # Parse the target triplet.
    453             if line.startswith ('Target is '):
    454                 self.target_line = line
    455                 continue
    456 
    457             # Parse the host triplet.
    458             if line.startswith ('Host   is '):
    459                 self.host_line = line
    460                 continue
    461 
    462             # Parse the acats premable.
    463             if line == '\t\t=== acats configuration ===\n':
    464                 self.parse_acats_run (filename, file)
    465                 continue
    466 
    467             # Parse the tool name.
    468             match = self.tool_re.match (line)
    469             if match:
    470                 tool = self.get_tool (match.group (1))
    471                 continue
    472 
    473             # Skip over the final summary (which we instead create from
    474             # individual runs) and parse the version output.
    475             if tool and line == '\t\t=== ' + tool.name + ' Summary ===\n':
    476                 if file.readline() != '\n':
    477                     self.fatal (filename, 'expected blank line after summary')
    478                 self.parse_final_summary (filename, file)
    479                 continue
    480 
    481             # Parse the completion line.  In the case where several files
    482             # are being parsed, pick the one with the latest time.
    483             match = self.completed_re.match (line)
    484             if match:
    485                 time = self.parse_time (match.group (1))
    486                 if not self.end_line or self.end_line[0] < time:
    487                     self.end_line = (time, line)
    488                 continue
    489 
    490             # Sanity check to make sure that important text doesn't get
    491             # dropped accidentally.
    492             if strict and line.strip() != '':
    493                 self.fatal (filename, 'unrecognised line: ' + line[:-1])
    494 
    495     # Output a segment of text.
    496     def output_segment (self, segment):
    497         with safe_open (segment.filename) as file:
    498             file.seek (segment.start)
    499             for i in range (segment.lines):
    500                 sys.stdout.write (file.readline())
    501 
    502     # Output a summary giving the number of times each type of result has
    503     # been seen.
    504     def output_summary (self, tool, counts):
    505         for i in range (len (self.count_names)):
    506             name = self.count_names[i]
    507             # dejagnu only prints result types that were seen at least once,
    508             # but acats always prints a number of unexpected failures.
    509             if (counts[i] > 0
    510                 or (tool.name == 'acats'
    511                     and name.startswith ('# of unexpected failures'))):
    512                 sys.stdout.write ('%s%d\n' % (name, counts[i]))
    513 
    514     # Output unified .log or .sum information for a particular variation,
    515     # with a summary at the end.
    516     def output_variation (self, tool, variation):
    517         self.output_segment (variation.header)
    518         for harness in sorted (variation.harnesses.values(),
    519                                key = attrgetter ('name')):
    520             sys.stdout.write ('Running ' + harness.name + ' ...\n')
    521             if self.do_sum:
    522                 harness.results.sort()
    523                 for (key, line) in harness.results:
    524                     sys.stdout.write (line)
    525             else:
    526                 # Rearrange the log segments into test order (but without
    527                 # rearranging text within those segments).
    528                 for key in sorted (harness.segments.keys()):
    529                     self.output_segment (harness.segments[key])
    530                 for segment in harness.empty:
    531                     self.output_segment (segment)
    532         if len (self.variations) > 1:
    533             sys.stdout.write ('\t\t=== ' + tool.name + ' Summary for '
    534                               + variation.name + ' ===\n\n')
    535             self.output_summary (tool, variation.counts)
    536 
    537     # Output unified .log or .sum information for a particular tool,
    538     # with a summary at the end.
    539     def output_tool (self, tool):
    540         counts = self.zero_counts()
    541         if tool.name == 'acats':
    542             # acats doesn't use variations, so just output everything.
    543             # It also has a different approach to whitespace.
    544             sys.stdout.write ('\t\t=== ' + tool.name + ' tests ===\n')
    545             for variation in tool.variations.values():
    546                 self.output_variation (tool, variation)
    547                 self.accumulate_counts (counts, variation.counts)
    548             sys.stdout.write ('\t\t=== ' + tool.name + ' Summary ===\n')
    549         else:
    550             # Output the results in the usual dejagnu runtest format.
    551             sys.stdout.write ('\n\t\t=== ' + tool.name + ' tests ===\n\n'
    552                               'Schedule of variations:\n')
    553             for name in self.variations:
    554                 if name in tool.variations:
    555                     sys.stdout.write ('    ' + name + '\n')
    556             sys.stdout.write ('\n')
    557             for name in self.variations:
    558                 if name in tool.variations:
    559                     variation = tool.variations[name]
    560                     sys.stdout.write ('Running target '
    561                                       + variation.name + '\n')
    562                     self.output_variation (tool, variation)
    563                     self.accumulate_counts (counts, variation.counts)
    564             sys.stdout.write ('\n\t\t=== ' + tool.name + ' Summary ===\n\n')
    565         self.output_summary (tool, counts)
    566 
    567     def main (self):
    568         self.parse_cmdline()
    569         try:
    570             # Parse the input files.
    571             for filename in self.files:
    572                 with safe_open (filename) as file:
    573                     self.parse_file (filename, file)
    574 
    575             # Decide what to output.
    576             if len (self.variations) == 0:
    577                 self.variations = sorted (self.known_variations)
    578             else:
    579                 for name in self.variations:
    580                     if name not in self.known_variations:
    581                         self.fatal (None, 'no results for ' + name)
    582             if len (self.tools) == 0:
    583                 self.tools = sorted (self.runs.keys())
    584 
    585             # Output the header.
    586             if self.start_line:
    587                 sys.stdout.write (self.start_line[1])
    588             sys.stdout.write (self.native_line)
    589             sys.stdout.write (self.target_line)
    590             sys.stdout.write (self.host_line)
    591             sys.stdout.write (self.acats_premable)
    592 
    593             # Output the main body.
    594             for name in self.tools:
    595                 if name not in self.runs:
    596                     self.fatal (None, 'no results for ' + name)
    597                 self.output_tool (self.runs[name])
    598 
    599             # Output the footer.
    600             if len (self.acats_failures) > 0:
    601                 sys.stdout.write ('*** FAILURES: '
    602                                   + ' '.join (self.acats_failures) + '\n')
    603             sys.stdout.write (self.version_output)
    604             if self.end_line:
    605                 sys.stdout.write (self.end_line[1])
    606         except IOError as e:
    607             self.fatal (e.filename, e.strerror)
    608 
    609 Prog().main()
    610