• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2011 Google Inc. All Rights Reserved.
2# Author: kbaclawski@google.com (Krystian Baclawski)
3#
4
5from collections import defaultdict
6from collections import namedtuple
7from datetime import datetime
8from fnmatch import fnmatch
9from itertools import groupby
10import logging
11import os.path
12import re
13
14
15class 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
104class 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