| #!/usr/bin/python |
| # |
| # Copyright (C) 2014-2024 Free Software Foundation, Inc. |
| # |
| # This script is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 3, or (at your option) |
| # any later version. |
| |
| import sys |
| import getopt |
| import re |
| import io |
| from datetime import datetime |
| from operator import attrgetter |
| |
| # True if unrecognised lines should cause a fatal error. Might want to turn |
| # this on by default later. |
| strict = False |
| |
| # True if the order of .log segments should match the .sum file, false if |
| # they should keep the original order. |
| sort_logs = True |
| |
| # A version of open() that is safe against whatever binary output |
| # might be added to the log. |
| def safe_open (filename): |
| if sys.version_info >= (3, 0): |
| return open (filename, 'r', errors = 'surrogateescape') |
| return open (filename, 'r') |
| |
| # Force stdout to handle escape sequences from a safe_open file. |
| if sys.version_info >= (3, 0): |
| sys.stdout = io.TextIOWrapper (sys.stdout.buffer, |
| errors = 'surrogateescape') |
| |
| class Named: |
| def __init__ (self, name): |
| self.name = name |
| |
| class ToolRun (Named): |
| def __init__ (self, name): |
| Named.__init__ (self, name) |
| # The variations run for this tool, mapped by --target_board name. |
| self.variations = dict() |
| |
| # Return the VariationRun for variation NAME. |
| def get_variation (self, name): |
| if name not in self.variations: |
| self.variations[name] = VariationRun (name) |
| return self.variations[name] |
| |
| class VariationRun (Named): |
| def __init__ (self, name): |
| Named.__init__ (self, name) |
| # A segment of text before the harness runs start, describing which |
| # baseboard files were loaded for the target. |
| self.header = None |
| # The harnesses run for this variation, mapped by filename. |
| self.harnesses = dict() |
| # A list giving the number of times each type of result has |
| # been seen. |
| self.counts = [] |
| |
| # Return the HarnessRun for harness NAME. |
| def get_harness (self, name): |
| if name not in self.harnesses: |
| self.harnesses[name] = HarnessRun (name) |
| return self.harnesses[name] |
| |
| class HarnessRun (Named): |
| def __init__ (self, name): |
| Named.__init__ (self, name) |
| # Segments of text that make up the harness run, mapped by a test-based |
| # key that can be used to order them. |
| self.segments = dict() |
| # Segments of text that make up the harness run but which have |
| # no recognized test results. These are typically harnesses that |
| # are completely skipped for the target. |
| self.empty = [] |
| # A list of results. Each entry is a pair in which the first element |
| # is a unique sorting key and in which the second is the full |
| # PASS/FAIL line. |
| self.results = [] |
| |
| # Add a segment of text to the harness run. If the segment includes |
| # test results, KEY is an example of one of them, and can be used to |
| # combine the individual segments in order. If the segment has no |
| # test results (e.g. because the harness doesn't do anything for the |
| # current configuration) then KEY is None instead. In that case |
| # just collect the segments in the order that we see them. |
| def add_segment (self, key, segment): |
| if key: |
| assert key not in self.segments |
| self.segments[key] = segment |
| else: |
| self.empty.append (segment) |
| |
| class Segment: |
| def __init__ (self, filename, start): |
| self.filename = filename |
| self.start = start |
| self.lines = 0 |
| |
| class Prog: |
| def __init__ (self): |
| # The variations specified on the command line. |
| self.variations = [] |
| # The variations seen in the input files. |
| self.known_variations = set() |
| # The tools specified on the command line. |
| self.tools = [] |
| # Whether to create .sum rather than .log output. |
| self.do_sum = True |
| # Regexps used while parsing. |
| self.test_run_re = re.compile (r'^Test run by (\S+) on (.*)$', |
| re.IGNORECASE) |
| self.tool_re = re.compile (r'^\t\t=== (.*) tests ===$') |
| self.result_re = re.compile (r'^(PASS|XPASS|FAIL|XFAIL|UNRESOLVED' |
| r'|WARNING|ERROR|UNSUPPORTED|UNTESTED' |
| r'|KFAIL|KPASS|PATH|DUPLICATE):\s*(.+)') |
| self.completed_re = re.compile (r'.* completed at (.*)') |
| # Pieces of text to write at the head of the output. |
| # start_line is a pair in which the first element is a datetime |
| # and in which the second is the associated 'Test Run By' line. |
| self.start_line = None |
| self.native_line = '' |
| self.target_line = '' |
| self.host_line = '' |
| self.acats_premable = '' |
| # Pieces of text to write at the end of the output. |
| # end_line is like start_line but for the 'runtest completed' line. |
| self.acats_failures = [] |
| self.version_output = '' |
| self.end_line = None |
| # Known summary types. |
| self.count_names = [ |
| '# of DejaGnu errors\t\t', |
| '# of expected passes\t\t', |
| '# of unexpected failures\t', |
| '# of unexpected successes\t', |
| '# of expected failures\t\t', |
| '# of unknown successes\t\t', |
| '# of known failures\t\t', |
| '# of untested testcases\t\t', |
| '# of unresolved testcases\t', |
| '# of unsupported tests\t\t', |
| '# of paths in test names\t', |
| '# of duplicate test names\t' |
| ] |
| self.runs = dict() |
| |
| def usage (self): |
| name = sys.argv[0] |
| sys.stderr.write ('Usage: ' + name |
| + ''' [-t tool] [-l variant-list] [-L] log-or-sum-file ... |
| |
| tool The tool (e.g. g++, libffi) for which to create a |
| new test summary file. If not specified then output |
| is created for all tools. |
| variant-list One or more test variant names. If the list is |
| not specified then one is constructed from all |
| variants in the files for <tool>. |
| sum-file A test summary file with the format of those |
| created by runtest from DejaGnu. |
| If -L is used, merge *.log files instead of *.sum. In this |
| mode the exact order of lines may not be preserved, just different |
| Running *.exp chunks should be in correct order. |
| ''') |
| sys.exit (1) |
| |
| def fatal (self, what, string): |
| if not what: |
| what = sys.argv[0] |
| sys.stderr.write (what + ': ' + string + '\n') |
| sys.exit (1) |
| |
| # Parse the command-line arguments. |
| def parse_cmdline (self): |
| try: |
| (options, self.files) = getopt.getopt (sys.argv[1:], 'l:t:L') |
| if len (self.files) == 0: |
| self.usage() |
| for (option, value) in options: |
| if option == '-l': |
| self.variations.append (value) |
| elif option == '-t': |
| self.tools.append (value) |
| else: |
| self.do_sum = False |
| except getopt.GetoptError as e: |
| self.fatal (None, e.msg) |
| |
| # Try to parse time string TIME, returning an arbitrary time on failure. |
| # Getting this right is just a nice-to-have so failures should be silent. |
| def parse_time (self, time): |
| try: |
| return datetime.strptime (time, '%c') |
| except ValueError: |
| return datetime.now() |
| |
| # Parse an integer and abort on failure. |
| def parse_int (self, filename, value): |
| try: |
| return int (value) |
| except ValueError: |
| self.fatal (filename, 'expected an integer, got: ' + value) |
| |
| # Return a list that represents no test results. |
| def zero_counts (self): |
| return [0 for x in self.count_names] |
| |
| # Return the ToolRun for tool NAME. |
| def get_tool (self, name): |
| if name not in self.runs: |
| self.runs[name] = ToolRun (name) |
| return self.runs[name] |
| |
| # Add the result counts in list FROMC to TOC. |
| def accumulate_counts (self, toc, fromc): |
| for i in range (len (self.count_names)): |
| toc[i] += fromc[i] |
| |
| # Parse the list of variations after 'Schedule of variations:'. |
| # Return the number seen. |
| def parse_variations (self, filename, file): |
| num_variations = 0 |
| while True: |
| line = file.readline() |
| if line == '': |
| self.fatal (filename, 'could not parse variation list') |
| if line == '\n': |
| break |
| self.known_variations.add (line.strip()) |
| num_variations += 1 |
| return num_variations |
| |
| # Parse from the first line after 'Running target ...' to the end |
| # of the run's summary. |
| def parse_run (self, filename, file, tool, variation, num_variations): |
| header = None |
| harness = None |
| segment = None |
| final_using = 0 |
| has_warning = 0 |
| |
| # If this is the first run for this variation, add any text before |
| # the first harness to the header. |
| if not variation.header: |
| segment = Segment (filename, file.tell()) |
| variation.header = segment |
| |
| # Parse the rest of the summary (the '# of ' lines). |
| if len (variation.counts) == 0: |
| variation.counts = self.zero_counts() |
| |
| # Parse up until the first line of the summary. |
| if num_variations == 1: |
| end = '\t\t=== ' + tool.name + ' Summary ===\n' |
| else: |
| end = ('\t\t=== ' + tool.name + ' Summary for ' |
| + variation.name + ' ===\n') |
| while True: |
| line = file.readline() |
| if line == '': |
| self.fatal (filename, 'no recognised summary line') |
| if line == end: |
| break |
| |
| # Look for the start of a new harness. |
| if line.startswith ('Running ') and line.endswith (' ...\n'): |
| # Close off the current harness segment, if any. |
| if harness: |
| segment.lines -= final_using |
| harness.add_segment (first_key, segment) |
| name = line[len ('Running '):-len(' ...\n')] |
| harness = variation.get_harness (name) |
| segment = Segment (filename, file.tell()) |
| first_key = None |
| final_using = 0 |
| continue |
| |
| # Record test results. Associate the first test result with |
| # the harness segment, so that if a run for a particular harness |
| # has been split up, we can reassemble the individual segments |
| # in a sensible order. |
| # |
| # dejagnu sometimes issues warnings about the testing environment |
| # before running any tests. Treat them as part of the header |
| # rather than as a test result. |
| 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') |
| name = match.group (2) |
| # Ugly hack to get the right order for gfortran. |
| if name.startswith ('gfortran.dg/g77/'): |
| name = 'h' + name |
| # If we have a time out warning, make sure it appears |
| # before the following testcase diagnostic: we insert |
| # the testname before 'program' so that sort faces a |
| # list of testnames. |
| if line.startswith ('WARNING: program timed out'): |
| has_warning = 1 |
| else: |
| if has_warning == 1: |
| key = (name, len (harness.results)) |
| myline = 'WARNING: %s program timed out.\n' % name |
| harness.results.append ((key, myline)) |
| has_warning = 0 |
| key = (name, len (harness.results)) |
| harness.results.append ((key, line)) |
| if not first_key and sort_logs: |
| first_key = key |
| if line.startswith ('ERROR: (DejaGnu)'): |
| for i in range (len (self.count_names)): |
| if 'DejaGnu errors' in self.count_names[i]: |
| variation.counts[i] += 1 |
| break |
| |
| # 'Using ...' lines are only interesting in a header. Splitting |
| # the test up into parallel runs leads to more 'Using ...' lines |
| # than there would be in a single log. |
| if line.startswith ('Using '): |
| final_using += 1 |
| else: |
| final_using = 0 |
| |
| # Add other text to the current segment, if any. |
| if segment: |
| segment.lines += 1 |
| |
| # Close off the final harness segment, if any. |
| if harness: |
| segment.lines -= final_using |
| harness.add_segment (first_key, segment) |
| |
| while True: |
| before = file.tell() |
| line = file.readline() |
| if line == '': |
| break |
| if line == '\n': |
| continue |
| if not line.startswith ('# '): |
| file.seek (before) |
| break |
| found = False |
| for i in range (len (self.count_names)): |
| if line.startswith (self.count_names[i]): |
| count = line[len (self.count_names[i]):-1].strip() |
| variation.counts[i] += self.parse_int (filename, count) |
| found = True |
| break |
| if not found: |
| self.fatal (filename, 'unknown test result: ' + line[:-1]) |
| |
| # Parse an acats run, which uses a different format from dejagnu. |
| # We have just skipped over '=== acats configuration ==='. |
| def parse_acats_run (self, filename, file): |
| # Parse the preamble, which describes the configuration and logs |
| # the creation of support files. |
| record = (self.acats_premable == '') |
| if record: |
| self.acats_premable = '\t\t=== acats configuration ===\n' |
| while True: |
| line = file.readline() |
| if line == '': |
| self.fatal (filename, 'could not parse acats preamble') |
| if line == '\t\t=== acats tests ===\n': |
| break |
| if record: |
| self.acats_premable += line |
| |
| # Parse the test results themselves, using a dummy variation name. |
| tool = self.get_tool ('acats') |
| variation = tool.get_variation ('none') |
| self.parse_run (filename, file, tool, variation, 1) |
| |
| # Parse the failure list. |
| while True: |
| before = file.tell() |
| line = file.readline() |
| if line.startswith ('*** FAILURES: '): |
| self.acats_failures.append (line[len ('*** FAILURES: '):-1]) |
| continue |
| file.seek (before) |
| break |
| |
| # Parse the final summary at the end of a log in order to capture |
| # the version output that follows it. |
| def parse_final_summary (self, filename, file): |
| record = (self.version_output == '') |
| while True: |
| line = file.readline() |
| if line == '': |
| break |
| if line.startswith ('# of '): |
| continue |
| if record: |
| self.version_output += line |
| if line == '\n': |
| break |
| |
| # Parse a .log or .sum file. |
| def parse_file (self, filename, file): |
| tool = None |
| target = None |
| num_variations = 1 |
| while True: |
| line = file.readline() |
| if line == '': |
| return |
| |
| # Parse the list of variations, which comes before the test |
| # runs themselves. |
| if line.startswith ('Schedule of variations:'): |
| num_variations = self.parse_variations (filename, file) |
| continue |
| |
| # Parse a testsuite run for one tool/variation combination. |
| if line.startswith ('Running target '): |
| name = line[len ('Running target '):-1] |
| if not tool: |
| self.fatal (filename, 'could not parse tool name') |
| if name not in self.known_variations: |
| self.fatal (filename, 'unknown target: ' + name) |
| self.parse_run (filename, file, tool, |
| tool.get_variation (name), |
| num_variations) |
| # If there is only one variation then there is no separate |
| # summary for it. Record any following version output. |
| if num_variations == 1: |
| self.parse_final_summary (filename, file) |
| continue |
| |
| # Parse the start line. In the case where several files are being |
| # parsed, pick the one with the earliest time. |
| match = self.test_run_re.match (line) |
| if match: |
| time = self.parse_time (match.group (2)) |
| if not self.start_line or self.start_line[0] > time: |
| self.start_line = (time, line) |
| continue |
| |
| # Parse the form used for native testing. |
| if line.startswith ('Native configuration is '): |
| self.native_line = line |
| continue |
| |
| # Parse the target triplet. |
| if line.startswith ('Target is '): |
| self.target_line = line |
| continue |
| |
| # Parse the host triplet. |
| if line.startswith ('Host is '): |
| self.host_line = line |
| continue |
| |
| # Parse the acats premable. |
| if line == '\t\t=== acats configuration ===\n': |
| self.parse_acats_run (filename, file) |
| continue |
| |
| # Parse the tool name. |
| match = self.tool_re.match (line) |
| if match: |
| tool = self.get_tool (match.group (1)) |
| continue |
| |
| # Skip over the final summary (which we instead create from |
| # 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') |
| self.parse_final_summary (filename, file) |
| continue |
| |
| # Parse the completion line. In the case where several files |
| # are being parsed, pick the one with the latest time. |
| match = self.completed_re.match (line) |
| if match: |
| time = self.parse_time (match.group (1)) |
| if not self.end_line or self.end_line[0] < time: |
| self.end_line = (time, line) |
| continue |
| |
| # 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]) |
| |
| # Output a segment of text. |
| def output_segment (self, segment): |
| with safe_open (segment.filename) as file: |
| file.seek (segment.start) |
| for i in range (segment.lines): |
| sys.stdout.write (file.readline()) |
| |
| # Output a summary giving the number of times each type of result has |
| # been seen. |
| def output_summary (self, tool, counts): |
| for i in range (len (self.count_names)): |
| name = self.count_names[i] |
| # dejagnu only prints result types that were seen at least once, |
| # but acats always prints a number of unexpected failures. |
| if (counts[i] > 0 |
| or (tool.name == 'acats' |
| and name.startswith ('# of unexpected failures'))): |
| sys.stdout.write ('%s%d\n' % (name, counts[i])) |
| |
| # Output unified .log or .sum information for a particular variation, |
| # with a summary at the end. |
| def output_variation (self, tool, variation): |
| self.output_segment (variation.header) |
| for harness in sorted (variation.harnesses.values(), |
| key = attrgetter ('name')): |
| sys.stdout.write ('Running ' + harness.name + ' ...\n') |
| if self.do_sum: |
| harness.results.sort() |
| for (key, line) in harness.results: |
| sys.stdout.write (line) |
| else: |
| # Rearrange the log segments into test order (but without |
| # rearranging text within those segments). |
| for key in sorted (harness.segments.keys()): |
| self.output_segment (harness.segments[key]) |
| for segment in harness.empty: |
| self.output_segment (segment) |
| if len (self.variations) > 1: |
| sys.stdout.write ('\t\t=== ' + tool.name + ' Summary for ' |
| + variation.name + ' ===\n\n') |
| self.output_summary (tool, variation.counts) |
| |
| # Output unified .log or .sum information for a particular tool, |
| # with a summary at the end. |
| def output_tool (self, tool): |
| counts = self.zero_counts() |
| if tool.name == 'acats': |
| # acats doesn't use variations, so just output everything. |
| # It also has a different approach to whitespace. |
| sys.stdout.write ('\t\t=== ' + tool.name + ' tests ===\n') |
| for variation in tool.variations.values(): |
| self.output_variation (tool, variation) |
| self.accumulate_counts (counts, variation.counts) |
| sys.stdout.write ('\t\t=== ' + tool.name + ' Summary ===\n') |
| else: |
| # Output the results in the usual dejagnu runtest format. |
| sys.stdout.write ('\n\t\t=== ' + tool.name + ' tests ===\n\n' |
| 'Schedule of variations:\n') |
| for name in self.variations: |
| if name in tool.variations: |
| sys.stdout.write (' ' + name + '\n') |
| sys.stdout.write ('\n') |
| for name in self.variations: |
| if name in tool.variations: |
| variation = tool.variations[name] |
| sys.stdout.write ('Running target ' |
| + variation.name + '\n') |
| self.output_variation (tool, variation) |
| self.accumulate_counts (counts, variation.counts) |
| sys.stdout.write ('\n\t\t=== ' + tool.name + ' Summary ===\n\n') |
| self.output_summary (tool, counts) |
| |
| def main (self): |
| self.parse_cmdline() |
| try: |
| # Parse the input files. |
| for filename in self.files: |
| with safe_open (filename) as file: |
| self.parse_file (filename, file) |
| |
| # Decide what to output. |
| if len (self.variations) == 0: |
| self.variations = sorted (self.known_variations) |
| else: |
| for name in self.variations: |
| if name not in self.known_variations: |
| self.fatal (None, 'no results for ' + name) |
| if len (self.tools) == 0: |
| self.tools = sorted (self.runs.keys()) |
| |
| # Output the header. |
| if self.start_line: |
| sys.stdout.write (self.start_line[1]) |
| sys.stdout.write (self.native_line) |
| sys.stdout.write (self.target_line) |
| sys.stdout.write (self.host_line) |
| sys.stdout.write (self.acats_premable) |
| |
| # Output the main body. |
| for name in self.tools: |
| if name not in self.runs: |
| self.fatal (None, 'no results for ' + name) |
| self.output_tool (self.runs[name]) |
| |
| # Output the footer. |
| if len (self.acats_failures) > 0: |
| sys.stdout.write ('*** FAILURES: ' |
| + ' '.join (self.acats_failures) + '\n') |
| sys.stdout.write (self.version_output) |
| if self.end_line: |
| sys.stdout.write (self.end_line[1]) |
| except IOError as e: |
| self.fatal (e.filename, e.strerror) |
| |
| Prog().main() |