| # 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) |