Home | History | Annotate | Line # | Download | only in contrib
dg-extract-results.py revision 1.1.1.2
      1 #!/usr/bin/python
      2 #
      3 # Copyright (C) 2014 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         self.tool_re = re.compile (r'^\t\t=== (.*) tests ===$')
    118         self.result_re = re.compile (r'^(PASS|XPASS|FAIL|XFAIL|UNRESOLVED'
    119                                      r'|WARNING|ERROR|UNSUPPORTED|UNTESTED'
    120                                      r'|KFAIL):\s*(.+)')
    121         self.completed_re = re.compile (r'.* completed at (.*)')
    122         # Pieces of text to write at the head of the output.
    123         # start_line is a pair in which the first element is a datetime
    124         # and in which the second is the associated 'Test Run By' line.
    125         self.start_line = None
    126         self.native_line = ''
    127         self.target_line = ''
    128         self.host_line = ''
    129         self.acats_premable = ''
    130         # Pieces of text to write at the end of the output.
    131         # end_line is like start_line but for the 'runtest completed' line.
    132         self.acats_failures = []
    133         self.version_output = ''
    134         self.end_line = None
    135         # Known summary types.
    136         self.count_names = [
    137             '# of DejaGnu errors\t\t',
    138             '# of expected passes\t\t',
    139             '# of unexpected failures\t',
    140             '# of unexpected successes\t',
    141             '# of expected failures\t\t',
    142             '# of unknown successes\t\t',
    143             '# of known failures\t\t',
    144             '# of untested testcases\t\t',
    145             '# of unresolved testcases\t',
    146             '# of unsupported tests\t\t'
    147         ]
    148         self.runs = dict()
    149 
    150     def usage (self):
    151         name = sys.argv[0]
    152         sys.stderr.write ('Usage: ' + name
    153                           + ''' [-t tool] [-l variant-list] [-L] log-or-sum-file ...
    154 
    155     tool           The tool (e.g. g++, libffi) for which to create a
    156                    new test summary file.  If not specified then output
    157                    is created for all tools.
    158     variant-list   One or more test variant names.  If the list is
    159                    not specified then one is constructed from all
    160                    variants in the files for <tool>.
    161     sum-file       A test summary file with the format of those
    162                    created by runtest from DejaGnu.
    163     If -L is used, merge *.log files instead of *.sum.  In this
    164     mode the exact order of lines may not be preserved, just different
    165     Running *.exp chunks should be in correct order.
    166 ''')
    167         sys.exit (1)
    168 
    169     def fatal (self, what, string):
    170         if not what:
    171             what = sys.argv[0]
    172         sys.stderr.write (what + ': ' + string + '\n')
    173         sys.exit (1)
    174 
    175     # Parse the command-line arguments.
    176     def parse_cmdline (self):
    177         try:
    178             (options, self.files) = getopt.getopt (sys.argv[1:], 'l:t:L')
    179             if len (self.files) == 0:
    180                 self.usage()
    181             for (option, value) in options:
    182                 if option == '-l':
    183                     self.variations.append (value)
    184                 elif option == '-t':
    185                     self.tools.append (value)
    186                 else:
    187                     self.do_sum = False
    188         except getopt.GetoptError as e:
    189             self.fatal (None, e.msg)
    190 
    191     # Try to parse time string TIME, returning an arbitrary time on failure.
    192     # Getting this right is just a nice-to-have so failures should be silent.
    193     def parse_time (self, time):
    194         try:
    195             return datetime.strptime (time, '%c')
    196         except ValueError:
    197             return datetime.now()
    198 
    199     # Parse an integer and abort on failure.
    200     def parse_int (self, filename, value):
    201         try:
    202             return int (value)
    203         except ValueError:
    204             self.fatal (filename, 'expected an integer, got: ' + value)
    205 
    206     # Return a list that represents no test results.
    207     def zero_counts (self):
    208         return [0 for x in self.count_names]
    209 
    210     # Return the ToolRun for tool NAME.
    211     def get_tool (self, name):
    212         if name not in self.runs:
    213             self.runs[name] = ToolRun (name)
    214         return self.runs[name]
    215 
    216     # Add the result counts in list FROMC to TOC.
    217     def accumulate_counts (self, toc, fromc):
    218         for i in range (len (self.count_names)):
    219             toc[i] += fromc[i]
    220 
    221     # Parse the list of variations after 'Schedule of variations:'.
    222     # Return the number seen.
    223     def parse_variations (self, filename, file):
    224         num_variations = 0
    225         while True:
    226             line = file.readline()
    227             if line == '':
    228                 self.fatal (filename, 'could not parse variation list')
    229             if line == '\n':
    230                 break
    231             self.known_variations.add (line.strip())
    232             num_variations += 1
    233         return num_variations
    234 
    235     # Parse from the first line after 'Running target ...' to the end
    236     # of the run's summary.
    237     def parse_run (self, filename, file, tool, variation, num_variations):
    238         header = None
    239         harness = None
    240         segment = None
    241         final_using = 0
    242 
    243         # If this is the first run for this variation, add any text before
    244         # the first harness to the header.
    245         if not variation.header:
    246             segment = Segment (filename, file.tell())
    247             variation.header = segment
    248 
    249         # Parse the rest of the summary (the '# of ' lines).
    250         if len (variation.counts) == 0:
    251             variation.counts = self.zero_counts()
    252 
    253         # Parse up until the first line of the summary.
    254         if num_variations == 1:
    255             end = '\t\t=== ' + tool.name + ' Summary ===\n'
    256         else:
    257             end = ('\t\t=== ' + tool.name + ' Summary for '
    258                    + variation.name + ' ===\n')
    259         while True:
    260             line = file.readline()
    261             if line == '':
    262                 self.fatal (filename, 'no recognised summary line')
    263             if line == end:
    264                 break
    265 
    266             # Look for the start of a new harness.
    267             if line.startswith ('Running ') and line.endswith (' ...\n'):
    268                 # Close off the current harness segment, if any.
    269                 if harness:
    270                     segment.lines -= final_using
    271                     harness.add_segment (first_key, segment)
    272                 name = line[len ('Running '):-len(' ...\n')]
    273                 harness = variation.get_harness (name)
    274                 segment = Segment (filename, file.tell())
    275                 first_key = None
    276                 final_using = 0
    277                 continue
    278 
    279             # Record test results.  Associate the first test result with
    280             # the harness segment, so that if a run for a particular harness
    281             # has been split up, we can reassemble the individual segments
    282             # in a sensible order.
    283             #
    284             # dejagnu sometimes issues warnings about the testing environment
    285             # before running any tests.  Treat them as part of the header
    286             # rather than as a test result.
    287             match = self.result_re.match (line)
    288             if match and (harness or not line.startswith ('WARNING:')):
    289                 if not harness:
    290                     self.fatal (filename, 'saw test result before harness name')
    291                 name = match.group (2)
    292                 # Ugly hack to get the right order for gfortran.
    293                 if name.startswith ('gfortran.dg/g77/'):
    294                     name = 'h' + name
    295                 key = (name, len (harness.results))
    296                 harness.results.append ((key, line))
    297                 if not first_key and sort_logs:
    298                     first_key = key
    299                 if line.startswith ('ERROR: (DejaGnu)'):
    300                     for i in range (len (self.count_names)):
    301                         if 'DejaGnu errors' in self.count_names[i]:
    302                             variation.counts[i] += 1
    303                             break
    304 
    305             # 'Using ...' lines are only interesting in a header.  Splitting
    306             # the test up into parallel runs leads to more 'Using ...' lines
    307             # than there would be in a single log.
    308             if line.startswith ('Using '):
    309                 final_using += 1
    310             else:
    311                 final_using = 0
    312 
    313             # Add other text to the current segment, if any.
    314             if segment:
    315                 segment.lines += 1
    316 
    317         # Close off the final harness segment, if any.
    318         if harness:
    319             segment.lines -= final_using
    320             harness.add_segment (first_key, segment)
    321 
    322         while True:
    323             before = file.tell()
    324             line = file.readline()
    325             if line == '':
    326                 break
    327             if line == '\n':
    328                 continue
    329             if not line.startswith ('# '):
    330                 file.seek (before)
    331                 break
    332             found = False
    333             for i in range (len (self.count_names)):
    334                 if line.startswith (self.count_names[i]):
    335                     count = line[len (self.count_names[i]):-1].strip()
    336                     variation.counts[i] += self.parse_int (filename, count)
    337                     found = True
    338                     break
    339             if not found:
    340                 self.fatal (filename, 'unknown test result: ' + line[:-1])
    341 
    342     # Parse an acats run, which uses a different format from dejagnu.
    343     # We have just skipped over '=== acats configuration ==='.
    344     def parse_acats_run (self, filename, file):
    345         # Parse the preamble, which describes the configuration and logs
    346         # the creation of support files.
    347         record = (self.acats_premable == '')
    348         if record:
    349             self.acats_premable = '\t\t=== acats configuration ===\n'
    350         while True:
    351             line = file.readline()
    352             if line == '':
    353                 self.fatal (filename, 'could not parse acats preamble')
    354             if line == '\t\t=== acats tests ===\n':
    355                 break
    356             if record:
    357                 self.acats_premable += line
    358 
    359         # Parse the test results themselves, using a dummy variation name.
    360         tool = self.get_tool ('acats')
    361         variation = tool.get_variation ('none')
    362         self.parse_run (filename, file, tool, variation, 1)
    363 
    364         # Parse the failure list.
    365         while True:
    366             before = file.tell()
    367             line = file.readline()
    368             if line.startswith ('*** FAILURES: '):
    369                 self.acats_failures.append (line[len ('*** FAILURES: '):-1])
    370                 continue
    371             file.seek (before)
    372             break
    373 
    374     # Parse the final summary at the end of a log in order to capture
    375     # the version output that follows it.
    376     def parse_final_summary (self, filename, file):
    377         record = (self.version_output == '')
    378         while True:
    379             line = file.readline()
    380             if line == '':
    381                 break
    382             if line.startswith ('# of '):
    383                 continue
    384             if record:
    385                 self.version_output += line
    386             if line == '\n':
    387                 break
    388 
    389     # Parse a .log or .sum file.
    390     def parse_file (self, filename, file):
    391         tool = None
    392         target = None
    393         num_variations = 1
    394         while True:
    395             line = file.readline()
    396             if line == '':
    397                 return
    398 
    399             # Parse the list of variations, which comes before the test
    400             # runs themselves.
    401             if line.startswith ('Schedule of variations:'):
    402                 num_variations = self.parse_variations (filename, file)
    403                 continue
    404 
    405             # Parse a testsuite run for one tool/variation combination.
    406             if line.startswith ('Running target '):
    407                 name = line[len ('Running target '):-1]
    408                 if not tool:
    409                     self.fatal (filename, 'could not parse tool name')
    410                 if name not in self.known_variations:
    411                     self.fatal (filename, 'unknown target: ' + name)
    412                 self.parse_run (filename, file, tool,
    413                                 tool.get_variation (name),
    414                                 num_variations)
    415                 # If there is only one variation then there is no separate
    416                 # summary for it.  Record any following version output.
    417                 if num_variations == 1:
    418                     self.parse_final_summary (filename, file)
    419                 continue
    420 
    421             # Parse the start line.  In the case where several files are being
    422             # parsed, pick the one with the earliest time.
    423             match = self.test_run_re.match (line)
    424             if match:
    425                 time = self.parse_time (match.group (2))
    426                 if not self.start_line or self.start_line[0] > time:
    427                     self.start_line = (time, line)
    428                 continue
    429 
    430             # Parse the form used for native testing.
    431             if line.startswith ('Native configuration is '):
    432                 self.native_line = line
    433                 continue
    434 
    435             # Parse the target triplet.
    436             if line.startswith ('Target is '):
    437                 self.target_line = line
    438                 continue
    439 
    440             # Parse the host triplet.
    441             if line.startswith ('Host   is '):
    442                 self.host_line = line
    443                 continue
    444 
    445             # Parse the acats premable.
    446             if line == '\t\t=== acats configuration ===\n':
    447                 self.parse_acats_run (filename, file)
    448                 continue
    449 
    450             # Parse the tool name.
    451             match = self.tool_re.match (line)
    452             if match:
    453                 tool = self.get_tool (match.group (1))
    454                 continue
    455 
    456             # Skip over the final summary (which we instead create from
    457             # individual runs) and parse the version output.
    458             if tool and line == '\t\t=== ' + tool.name + ' Summary ===\n':
    459                 if file.readline() != '\n':
    460                     self.fatal (filename, 'expected blank line after summary')
    461                 self.parse_final_summary (filename, file)
    462                 continue
    463 
    464             # Parse the completion line.  In the case where several files
    465             # are being parsed, pick the one with the latest time.
    466             match = self.completed_re.match (line)
    467             if match:
    468                 time = self.parse_time (match.group (1))
    469                 if not self.end_line or self.end_line[0] < time:
    470                     self.end_line = (time, line)
    471                 continue
    472 
    473             # Sanity check to make sure that important text doesn't get
    474             # dropped accidentally.
    475             if strict and line.strip() != '':
    476                 self.fatal (filename, 'unrecognised line: ' + line[:-1])
    477 
    478     # Output a segment of text.
    479     def output_segment (self, segment):
    480         with safe_open (segment.filename) as file:
    481             file.seek (segment.start)
    482             for i in range (segment.lines):
    483                 sys.stdout.write (file.readline())
    484 
    485     # Output a summary giving the number of times each type of result has
    486     # been seen.
    487     def output_summary (self, tool, counts):
    488         for i in range (len (self.count_names)):
    489             name = self.count_names[i]
    490             # dejagnu only prints result types that were seen at least once,
    491             # but acats always prints a number of unexpected failures.
    492             if (counts[i] > 0
    493                 or (tool.name == 'acats'
    494                     and name.startswith ('# of unexpected failures'))):
    495                 sys.stdout.write ('%s%d\n' % (name, counts[i]))
    496 
    497     # Output unified .log or .sum information for a particular variation,
    498     # with a summary at the end.
    499     def output_variation (self, tool, variation):
    500         self.output_segment (variation.header)
    501         for harness in sorted (variation.harnesses.values(),
    502                                key = attrgetter ('name')):
    503             sys.stdout.write ('Running ' + harness.name + ' ...\n')
    504             if self.do_sum:
    505                 harness.results.sort()
    506                 for (key, line) in harness.results:
    507                     sys.stdout.write (line)
    508             else:
    509                 # Rearrange the log segments into test order (but without
    510                 # rearranging text within those segments).
    511                 for key in sorted (harness.segments.keys()):
    512                     self.output_segment (harness.segments[key])
    513                 for segment in harness.empty:
    514                     self.output_segment (segment)
    515         if len (self.variations) > 1:
    516             sys.stdout.write ('\t\t=== ' + tool.name + ' Summary for '
    517                               + variation.name + ' ===\n\n')
    518             self.output_summary (tool, variation.counts)
    519 
    520     # Output unified .log or .sum information for a particular tool,
    521     # with a summary at the end.
    522     def output_tool (self, tool):
    523         counts = self.zero_counts()
    524         if tool.name == 'acats':
    525             # acats doesn't use variations, so just output everything.
    526             # It also has a different approach to whitespace.
    527             sys.stdout.write ('\t\t=== ' + tool.name + ' tests ===\n')
    528             for variation in tool.variations.values():
    529                 self.output_variation (tool, variation)
    530                 self.accumulate_counts (counts, variation.counts)
    531             sys.stdout.write ('\t\t=== ' + tool.name + ' Summary ===\n')
    532         else:
    533             # Output the results in the usual dejagnu runtest format.
    534             sys.stdout.write ('\n\t\t=== ' + tool.name + ' tests ===\n\n'
    535                               'Schedule of variations:\n')
    536             for name in self.variations:
    537                 if name in tool.variations:
    538                     sys.stdout.write ('    ' + name + '\n')
    539             sys.stdout.write ('\n')
    540             for name in self.variations:
    541                 if name in tool.variations:
    542                     variation = tool.variations[name]
    543                     sys.stdout.write ('Running target '
    544                                       + variation.name + '\n')
    545                     self.output_variation (tool, variation)
    546                     self.accumulate_counts (counts, variation.counts)
    547             sys.stdout.write ('\n\t\t=== ' + tool.name + ' Summary ===\n\n')
    548         self.output_summary (tool, counts)
    549 
    550     def main (self):
    551         self.parse_cmdline()
    552         try:
    553             # Parse the input files.
    554             for filename in self.files:
    555                 with safe_open (filename) as file:
    556                     self.parse_file (filename, file)
    557 
    558             # Decide what to output.
    559             if len (self.variations) == 0:
    560                 self.variations = sorted (self.known_variations)
    561             else:
    562                 for name in self.variations:
    563                     if name not in self.known_variations:
    564                         self.fatal (None, 'no results for ' + name)
    565             if len (self.tools) == 0:
    566                 self.tools = sorted (self.runs.keys())
    567 
    568             # Output the header.
    569             if self.start_line:
    570                 sys.stdout.write (self.start_line[1])
    571             sys.stdout.write (self.native_line)
    572             sys.stdout.write (self.target_line)
    573             sys.stdout.write (self.host_line)
    574             sys.stdout.write (self.acats_premable)
    575 
    576             # Output the main body.
    577             for name in self.tools:
    578                 if name not in self.runs:
    579                     self.fatal (None, 'no results for ' + name)
    580                 self.output_tool (self.runs[name])
    581 
    582             # Output the footer.
    583             if len (self.acats_failures) > 0:
    584                 sys.stdout.write ('*** FAILURES: '
    585                                   + ' '.join (self.acats_failures) + '\n')
    586             sys.stdout.write (self.version_output)
    587             if self.end_line:
    588                 sys.stdout.write (self.end_line[1])
    589         except IOError as e:
    590             self.fatal (e.filename, e.strerror)
    591 
    592 Prog().main()
    593