You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
8.1 KiB
262 lines
8.1 KiB
# Copyright 2011 Google Inc. All Rights Reserved. |
|
# Author: kbaclawski@google.com (Krystian Baclawski) |
|
# |
|
|
|
from collections import defaultdict |
|
from collections import namedtuple |
|
from datetime import datetime |
|
from fnmatch import fnmatch |
|
from itertools import groupby |
|
import logging |
|
import os.path |
|
import re |
|
|
|
|
|
class DejaGnuTestResult(namedtuple('Result', 'name variant result flaky')): |
|
"""Stores the result of a single test case.""" |
|
|
|
# avoid adding __dict__ to the class |
|
__slots__ = () |
|
|
|
LINE_RE = re.compile(r'([A-Z]+):\s+([\w/+.-]+)(.*)') |
|
|
|
@classmethod |
|
def FromLine(cls, line): |
|
"""Alternate constructor which takes a string and parses it.""" |
|
try: |
|
attrs, line = line.split('|', 1) |
|
|
|
if attrs.strip() != 'flaky': |
|
return None |
|
|
|
line = line.strip() |
|
flaky = True |
|
except ValueError: |
|
flaky = False |
|
|
|
fields = cls.LINE_RE.match(line.strip()) |
|
|
|
if fields: |
|
result, path, variant = fields.groups() |
|
|
|
# some of the tests are generated in build dir and are issued from there, |
|
# because every test run is performed in randomly named tmp directory we |
|
# need to remove random part |
|
try: |
|
# assume that 2nd field is a test path |
|
path_parts = path.split('/') |
|
|
|
index = path_parts.index('testsuite') |
|
path = '/'.join(path_parts[index + 1:]) |
|
except ValueError: |
|
path = '/'.join(path_parts) |
|
|
|
# Remove junk from test description. |
|
variant = variant.strip(', ') |
|
|
|
substitutions = [ |
|
# remove include paths - they contain name of tmp directory |
|
('-I\S+', ''), |
|
# compress white spaces |
|
('\s+', ' ') |
|
] |
|
|
|
for pattern, replacement in substitutions: |
|
variant = re.sub(pattern, replacement, variant) |
|
|
|
# Some tests separate last component of path by space, so actual filename |
|
# ends up in description instead of path part. Correct that. |
|
try: |
|
first, rest = variant.split(' ', 1) |
|
except ValueError: |
|
pass |
|
else: |
|
if first.endswith('.o'): |
|
path = os.path.join(path, first) |
|
variant = rest |
|
|
|
# DejaGNU framework errors don't contain path part at all, so description |
|
# part has to be reconstructed. |
|
if not any(os.path.basename(path).endswith('.%s' % suffix) |
|
for suffix in ['h', 'c', 'C', 'S', 'H', 'cc', 'i', 'o']): |
|
variant = '%s %s' % (path, variant) |
|
path = '' |
|
|
|
# Some tests are picked up from current directory (presumably DejaGNU |
|
# generates some test files). Remove the prefix for these files. |
|
if path.startswith('./'): |
|
path = path[2:] |
|
|
|
return cls(path, variant or '', result, flaky=flaky) |
|
|
|
def __str__(self): |
|
"""Returns string representation of a test result.""" |
|
if self.flaky: |
|
fmt = 'flaky | ' |
|
else: |
|
fmt = '' |
|
fmt += '{2}: {0}' |
|
if self.variant: |
|
fmt += ' {1}' |
|
return fmt.format(*self) |
|
|
|
|
|
class DejaGnuTestRun(object): |
|
"""Container for test results that were a part of single test run. |
|
|
|
The class stores also metadata related to the test run. |
|
|
|
Attributes: |
|
board: Name of DejaGNU board, which was used to run the tests. |
|
date: The date when the test run was started. |
|
target: Target triple. |
|
host: Host triple. |
|
tool: The tool that was tested (e.g. gcc, binutils, g++, etc.) |
|
results: a list of DejaGnuTestResult objects. |
|
""" |
|
|
|
__slots__ = ('board', 'date', 'target', 'host', 'tool', 'results') |
|
|
|
def __init__(self, **kwargs): |
|
assert all(name in self.__slots__ for name in kwargs) |
|
|
|
self.results = set() |
|
self.date = kwargs.get('date', datetime.now()) |
|
|
|
for name in ('board', 'target', 'tool', 'host'): |
|
setattr(self, name, kwargs.get(name, 'unknown')) |
|
|
|
@classmethod |
|
def FromFile(cls, filename): |
|
"""Alternate constructor - reads a DejaGNU output file.""" |
|
test_run = cls() |
|
test_run.FromDejaGnuOutput(filename) |
|
test_run.CleanUpTestResults() |
|
return test_run |
|
|
|
@property |
|
def summary(self): |
|
"""Returns a summary as {ResultType -> Count} dictionary.""" |
|
summary = defaultdict(int) |
|
|
|
for r in self.results: |
|
summary[r.result] += 1 |
|
|
|
return summary |
|
|
|
def _ParseBoard(self, fields): |
|
self.board = fields.group(1).strip() |
|
|
|
def _ParseDate(self, fields): |
|
self.date = datetime.strptime(fields.group(2).strip(), '%a %b %d %X %Y') |
|
|
|
def _ParseTarget(self, fields): |
|
self.target = fields.group(2).strip() |
|
|
|
def _ParseHost(self, fields): |
|
self.host = fields.group(2).strip() |
|
|
|
def _ParseTool(self, fields): |
|
self.tool = fields.group(1).strip() |
|
|
|
def FromDejaGnuOutput(self, filename): |
|
"""Read in and parse DejaGNU output file.""" |
|
|
|
logging.info('Reading "%s" DejaGNU output file.', filename) |
|
|
|
with open(filename, 'r') as report: |
|
lines = [line.strip() for line in report.readlines() if line.strip()] |
|
|
|
parsers = ((re.compile(r'Running target (.*)'), self._ParseBoard), |
|
(re.compile(r'Test Run By (.*) on (.*)'), self._ParseDate), |
|
(re.compile(r'=== (.*) tests ==='), self._ParseTool), |
|
(re.compile(r'Target(\s+)is (.*)'), self._ParseTarget), |
|
(re.compile(r'Host(\s+)is (.*)'), self._ParseHost)) |
|
|
|
for line in lines: |
|
result = DejaGnuTestResult.FromLine(line) |
|
|
|
if result: |
|
self.results.add(result) |
|
else: |
|
for regexp, parser in parsers: |
|
fields = regexp.match(line) |
|
if fields: |
|
parser(fields) |
|
break |
|
|
|
logging.debug('DejaGNU output file parsed successfully.') |
|
logging.debug(self) |
|
|
|
def CleanUpTestResults(self): |
|
"""Remove certain test results considered to be spurious. |
|
|
|
1) Large number of test reported as UNSUPPORTED are also marked as |
|
UNRESOLVED. If that's the case remove latter result. |
|
2) If a test is performed on compiler output and for some reason compiler |
|
fails, we don't want to report all failures that depend on the former. |
|
""" |
|
name_key = lambda v: v.name |
|
results_by_name = sorted(self.results, key=name_key) |
|
|
|
for name, res_iter in groupby(results_by_name, key=name_key): |
|
results = set(res_iter) |
|
|
|
# If DejaGnu was unable to compile a test it will create following result: |
|
failed = DejaGnuTestResult(name, '(test for excess errors)', 'FAIL', |
|
False) |
|
|
|
# If a test compilation failed, remove all results that are dependent. |
|
if failed in results: |
|
dependants = set(filter(lambda r: r.result != 'FAIL', results)) |
|
|
|
self.results -= dependants |
|
|
|
for res in dependants: |
|
logging.info('Removed {%s} dependance.', res) |
|
|
|
# Remove all UNRESOLVED results that were also marked as UNSUPPORTED. |
|
unresolved = [res._replace(result='UNRESOLVED') |
|
for res in results if res.result == 'UNSUPPORTED'] |
|
|
|
for res in unresolved: |
|
if res in self.results: |
|
self.results.remove(res) |
|
logging.info('Removed {%s} duplicate.', res) |
|
|
|
def _IsApplicable(self, manifest): |
|
"""Checks if test results need to be reconsidered based on the manifest.""" |
|
check_list = [(self.tool, manifest.tool), (self.board, manifest.board)] |
|
|
|
return all(fnmatch(text, pattern) for text, pattern in check_list) |
|
|
|
def SuppressTestResults(self, manifests): |
|
"""Suppresses all test results listed in manifests.""" |
|
|
|
# Get a set of tests results that are going to be suppressed if they fail. |
|
manifest_results = set() |
|
|
|
for manifest in filter(self._IsApplicable, manifests): |
|
manifest_results |= set(manifest.results) |
|
|
|
suppressed_results = self.results & manifest_results |
|
|
|
for result in sorted(suppressed_results): |
|
logging.debug('Result suppressed for {%s}.', result) |
|
|
|
new_result = '!' + result.result |
|
|
|
# Mark result suppression as applied. |
|
manifest_results.remove(result) |
|
|
|
# Rewrite test result. |
|
self.results.remove(result) |
|
self.results.add(result._replace(result=new_result)) |
|
|
|
for result in sorted(manifest_results): |
|
logging.warning('Result {%s} listed in manifest but not suppressed.', |
|
result) |
|
|
|
def __str__(self): |
|
return '{0}, {1} @{2} on {3}'.format(self.target, self.tool, self.board, |
|
self.date)
|
|
|