Home | History | Annotate | Download | only in dejagnu
      1 # Copyright 2011 Google Inc. All Rights Reserved.
      2 # Author: kbaclawski (at] google.com (Krystian Baclawski)
      3 #
      4 
      5 from collections import defaultdict
      6 from collections import namedtuple
      7 from datetime import datetime
      8 from fnmatch import fnmatch
      9 from itertools import groupby
     10 import logging
     11 import os.path
     12 import re
     13 
     14 
     15 class DejaGnuTestResult(namedtuple('Result', 'name variant result flaky')):
     16   """Stores the result of a single test case."""
     17 
     18   # avoid adding __dict__ to the class
     19   __slots__ = ()
     20 
     21   LINE_RE = re.compile(r'([A-Z]+):\s+([\w/+.-]+)(.*)')
     22 
     23   @classmethod
     24   def FromLine(cls, line):
     25     """Alternate constructor which takes a string and parses it."""
     26     try:
     27       attrs, line = line.split('|', 1)
     28 
     29       if attrs.strip() != 'flaky':
     30         return None
     31 
     32       line = line.strip()
     33       flaky = True
     34     except ValueError:
     35       flaky = False
     36 
     37     fields = cls.LINE_RE.match(line.strip())
     38 
     39     if fields:
     40       result, path, variant = fields.groups()
     41 
     42       # some of the tests are generated in build dir and are issued from there,
     43       # because every test run is performed in randomly named tmp directory we
     44       # need to remove random part
     45       try:
     46         # assume that 2nd field is a test path
     47         path_parts = path.split('/')
     48 
     49         index = path_parts.index('testsuite')
     50         path = '/'.join(path_parts[index + 1:])
     51       except ValueError:
     52         path = '/'.join(path_parts)
     53 
     54       # Remove junk from test description.
     55       variant = variant.strip(', ')
     56 
     57       substitutions = [
     58           # remove include paths - they contain name of tmp directory
     59           ('-I\S+', ''),
     60           # compress white spaces
     61           ('\s+', ' ')
     62       ]
     63 
     64       for pattern, replacement in substitutions:
     65         variant = re.sub(pattern, replacement, variant)
     66 
     67       # Some tests separate last component of path by space, so actual filename
     68       # ends up in description instead of path part. Correct that.
     69       try:
     70         first, rest = variant.split(' ', 1)
     71       except ValueError:
     72         pass
     73       else:
     74         if first.endswith('.o'):
     75           path = os.path.join(path, first)
     76           variant = rest
     77 
     78       # DejaGNU framework errors don't contain path part at all, so description
     79       # part has to be reconstructed.
     80       if not any(os.path.basename(path).endswith('.%s' % suffix)
     81                  for suffix in ['h', 'c', 'C', 'S', 'H', 'cc', 'i', 'o']):
     82         variant = '%s %s' % (path, variant)
     83         path = ''
     84 
     85       # Some tests are picked up from current directory (presumably DejaGNU
     86       # generates some test files). Remove the prefix for these files.
     87       if path.startswith('./'):
     88         path = path[2:]
     89 
     90       return cls(path, variant or '', result, flaky=flaky)
     91 
     92   def __str__(self):
     93     """Returns string representation of a test result."""
     94     if self.flaky:
     95       fmt = 'flaky | '
     96     else:
     97       fmt = ''
     98     fmt += '{2}: {0}'
     99     if self.variant:
    100       fmt += ' {1}'
    101     return fmt.format(*self)
    102 
    103 
    104 class DejaGnuTestRun(object):
    105   """Container for test results that were a part of single test run.
    106 
    107   The class stores also metadata related to the test run.
    108 
    109   Attributes:
    110     board: Name of DejaGNU board, which was used to run the tests.
    111     date: The date when the test run was started.
    112     target: Target triple.
    113     host: Host triple.
    114     tool: The tool that was tested (e.g. gcc, binutils, g++, etc.)
    115     results: a list of DejaGnuTestResult objects.
    116   """
    117 
    118   __slots__ = ('board', 'date', 'target', 'host', 'tool', 'results')
    119 
    120   def __init__(self, **kwargs):
    121     assert all(name in self.__slots__ for name in kwargs)
    122 
    123     self.results = set()
    124     self.date = kwargs.get('date', datetime.now())
    125 
    126     for name in ('board', 'target', 'tool', 'host'):
    127       setattr(self, name, kwargs.get(name, 'unknown'))
    128 
    129   @classmethod
    130   def FromFile(cls, filename):
    131     """Alternate constructor - reads a DejaGNU output file."""
    132     test_run = cls()
    133     test_run.FromDejaGnuOutput(filename)
    134     test_run.CleanUpTestResults()
    135     return test_run
    136 
    137   @property
    138   def summary(self):
    139     """Returns a summary as {ResultType -> Count} dictionary."""
    140     summary = defaultdict(int)
    141 
    142     for r in self.results:
    143       summary[r.result] += 1
    144 
    145     return summary
    146 
    147   def _ParseBoard(self, fields):
    148     self.board = fields.group(1).strip()
    149 
    150   def _ParseDate(self, fields):
    151     self.date = datetime.strptime(fields.group(2).strip(), '%a %b %d %X %Y')
    152 
    153   def _ParseTarget(self, fields):
    154     self.target = fields.group(2).strip()
    155 
    156   def _ParseHost(self, fields):
    157     self.host = fields.group(2).strip()
    158 
    159   def _ParseTool(self, fields):
    160     self.tool = fields.group(1).strip()
    161 
    162   def FromDejaGnuOutput(self, filename):
    163     """Read in and parse DejaGNU output file."""
    164 
    165     logging.info('Reading "%s" DejaGNU output file.', filename)
    166 
    167     with open(filename, 'r') as report:
    168       lines = [line.strip() for line in report.readlines() if line.strip()]
    169 
    170     parsers = ((re.compile(r'Running target (.*)'), self._ParseBoard),
    171                (re.compile(r'Test Run By (.*) on (.*)'), self._ParseDate),
    172                (re.compile(r'=== (.*) tests ==='), self._ParseTool),
    173                (re.compile(r'Target(\s+)is (.*)'), self._ParseTarget),
    174                (re.compile(r'Host(\s+)is (.*)'), self._ParseHost))
    175 
    176     for line in lines:
    177       result = DejaGnuTestResult.FromLine(line)
    178 
    179       if result:
    180         self.results.add(result)
    181       else:
    182         for regexp, parser in parsers:
    183           fields = regexp.match(line)
    184           if fields:
    185             parser(fields)
    186             break
    187 
    188     logging.debug('DejaGNU output file parsed successfully.')
    189     logging.debug(self)
    190 
    191   def CleanUpTestResults(self):
    192     """Remove certain test results considered to be spurious.
    193 
    194     1) Large number of test reported as UNSUPPORTED are also marked as
    195        UNRESOLVED. If that's the case remove latter result.
    196     2) If a test is performed on compiler output and for some reason compiler
    197        fails, we don't want to report all failures that depend on the former.
    198     """
    199     name_key = lambda v: v.name
    200     results_by_name = sorted(self.results, key=name_key)
    201 
    202     for name, res_iter in groupby(results_by_name, key=name_key):
    203       results = set(res_iter)
    204 
    205       # If DejaGnu was unable to compile a test it will create following result:
    206       failed = DejaGnuTestResult(name, '(test for excess errors)', 'FAIL',
    207                                  False)
    208 
    209       # If a test compilation failed, remove all results that are dependent.
    210       if failed in results:
    211         dependants = set(filter(lambda r: r.result != 'FAIL', results))
    212 
    213         self.results -= dependants
    214 
    215         for res in dependants:
    216           logging.info('Removed {%s} dependance.', res)
    217 
    218       # Remove all UNRESOLVED results that were also marked as UNSUPPORTED.
    219       unresolved = [res._replace(result='UNRESOLVED')
    220                     for res in results if res.result == 'UNSUPPORTED']
    221 
    222       for res in unresolved:
    223         if res in self.results:
    224           self.results.remove(res)
    225           logging.info('Removed {%s} duplicate.', res)
    226 
    227   def _IsApplicable(self, manifest):
    228     """Checks if test results need to be reconsidered based on the manifest."""
    229     check_list = [(self.tool, manifest.tool), (self.board, manifest.board)]
    230 
    231     return all(fnmatch(text, pattern) for text, pattern in check_list)
    232 
    233   def SuppressTestResults(self, manifests):
    234     """Suppresses all test results listed in manifests."""
    235 
    236     # Get a set of tests results that are going to be suppressed if they fail.
    237     manifest_results = set()
    238 
    239     for manifest in filter(self._IsApplicable, manifests):
    240       manifest_results |= set(manifest.results)
    241 
    242     suppressed_results = self.results & manifest_results
    243 
    244     for result in sorted(suppressed_results):
    245       logging.debug('Result suppressed for {%s}.', result)
    246 
    247       new_result = '!' + result.result
    248 
    249       # Mark result suppression as applied.
    250       manifest_results.remove(result)
    251 
    252       # Rewrite test result.
    253       self.results.remove(result)
    254       self.results.add(result._replace(result=new_result))
    255 
    256     for result in sorted(manifest_results):
    257       logging.warning('Result {%s} listed in manifest but not suppressed.',
    258                       result)
    259 
    260   def __str__(self):
    261     return '{0}, {1} @{2} on {3}'.format(self.target, self.tool, self.board,
    262                                          self.date)
    263