aboutsummaryrefslogtreecommitdiff
path: root/deprecated/automation/clients/report/dejagnu/summary.py
blob: d573c6914a09dda1cf0da8ceb8f537565aad0881 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# 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)