#!/usr/bin/python # # Copyright (C) 2014-2015 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 (.*)$') 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):\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 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' ] 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 . 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 # 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 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 key = (name, len (harness.results)) harness.results.append ((key, line)) if not first_key and sort_logs: first_key = key # '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) # Parse the rest of the summary (the '# of ' lines). if len (variation.counts) == 0: variation.counts = self.zero_counts() 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()