• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 the V8 project authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6Suppressions for V8 correctness fuzzer failures.
7
8We support three types of suppressions:
91. Ignore test case by pattern.
10Map a regular expression to a bug entry. A new failure will be reported
11when the pattern matches a JS test case.
12Subsequent matches will be recoreded under the first failure.
13
142. Ignore test run by output pattern:
15Map a regular expression to a bug entry. A new failure will be reported
16when the pattern matches the output of a particular run.
17Subsequent matches will be recoreded under the first failure.
18
193. Relax line-to-line comparisons with expressions of lines to ignore and
20lines to be normalized (i.e. ignore only portions of lines).
21These are not tied to bugs, be careful to not silently switch off this tool!
22
23Alternatively, think about adding a behavior change to v8_suppressions.js
24to silence a particular class of problems.
25"""
26
27import itertools
28import re
29
30try:
31  # Python 3
32  from itertools import zip_longest
33except ImportError:
34  # Python 2
35  from itertools import izip_longest as zip_longest
36
37# Max line length for regular experessions checking for lines to ignore.
38MAX_LINE_LENGTH = 512
39
40# For ignoring lines before carets and to ignore caret positions.
41CARET_RE = re.compile(r'^\s*\^\s*$')
42
43# Ignore by original source files. Map from bug->list of relative file paths,
44# e.g. 'v8/test/mjsunit/d8-performance-now.js'. A test will be suppressed if
45# one of the files below was used to mutate the test.
46IGNORE_SOURCES = {
47}
48
49# Ignore by test case pattern. Map from bug->regexp.
50# Bug is preferred to be a crbug.com/XYZ, but can be any short distinguishable
51# label.
52# Regular expressions are assumed to be compiled. We use regexp.search.
53IGNORE_TEST_CASES = {
54}
55
56# Ignore by output pattern. Map from bug->regexp like above.
57IGNORE_OUTPUT = {
58  'crbug.com/689877':
59      re.compile(r'^.*SyntaxError: .*Stack overflow$', re.M),
60  '_fake_difference_':
61      re.compile(r'^.*___fake_difference___$', re.M),
62}
63
64# Lines matching any of the following regular expressions will be ignored
65# if appearing on both sides. The capturing groups need to match exactly.
66# Use uncompiled regular expressions - they'll be compiled later.
67ALLOWED_LINE_DIFFS = [
68  # Ignore caret position in stack traces.
69  r'^\s*\^\s*$',
70]
71
72# Lines matching any of the following regular expressions will be ignored.
73# Use uncompiled regular expressions - they'll be compiled later.
74IGNORE_LINES = [
75  r'^Warning: .+ is deprecated.*$',
76  r'^Try --help for options$',
77
78  # crbug.com/705962
79  r'^\s\[0x[0-9a-f]+\]$',
80]
81
82
83###############################################################################
84# Implementation - you should not need to change anything below this point.
85
86# Compile regular expressions.
87ALLOWED_LINE_DIFFS = [re.compile(exp) for exp in ALLOWED_LINE_DIFFS]
88IGNORE_LINES = [re.compile(exp) for exp in IGNORE_LINES]
89
90ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: '
91
92
93def get_output_capped(output1, output2):
94  """Returns a pair of stdout strings.
95
96  The strings are safely capped if at least one run has crashed.
97  """
98
99  # No length difference or no crash -> no capping.
100  if (len(output1.stdout) == len(output2.stdout) or
101      (not output1.HasCrashed() and not output2.HasCrashed())):
102    return output1.stdout, output2.stdout
103
104  # Both runs have crashed, cap by the shorter output.
105  if output1.HasCrashed() and output2.HasCrashed():
106    cap = min(len(output1.stdout), len(output2.stdout))
107  # Only the first run has crashed, cap by its output length.
108  elif output1.HasCrashed():
109    cap = len(output1.stdout)
110  # Similar if only the second run has crashed.
111  else:
112    cap = len(output2.stdout)
113
114  return output1.stdout[0:cap], output2.stdout[0:cap]
115
116
117def line_pairs(lines):
118  return zip_longest(
119      lines, itertools.islice(lines, 1, None), fillvalue=None)
120
121
122def caret_match(line1, line2):
123  if (not line1 or
124      not line2 or
125      len(line1) > MAX_LINE_LENGTH or
126      len(line2) > MAX_LINE_LENGTH):
127    return False
128  return bool(CARET_RE.match(line1) and CARET_RE.match(line2))
129
130
131def short_line_output(line):
132  if len(line) <= MAX_LINE_LENGTH:
133    # Avoid copying.
134    return line
135  return line[0:MAX_LINE_LENGTH] + '...'
136
137
138def ignore_by_regexp(line1, line2, allowed):
139  if len(line1) > MAX_LINE_LENGTH or len(line2) > MAX_LINE_LENGTH:
140    return False
141  for exp in allowed:
142    match1 = exp.match(line1)
143    match2 = exp.match(line2)
144    if match1 and match2:
145      # If there are groups in the regexp, ensure the groups matched the same
146      # things.
147      if match1.groups() == match2.groups():  # tuple comparison
148        return True
149  return False
150
151
152def diff_output(output1, output2, allowed, ignore1, ignore2):
153  """Returns a tuple (difference, source).
154
155  The difference is None if there's no difference, otherwise a string
156  with a readable diff.
157
158  The source is the last source output within the test case, or None if no
159  such output existed.
160  """
161  def useful_line(ignore):
162    def fun(line):
163      return all(not e.match(line) for e in ignore)
164    return fun
165
166  lines1 = list(filter(useful_line(ignore1), output1))
167  lines2 = list(filter(useful_line(ignore2), output2))
168
169  # This keeps track where we are in the original source file of the fuzz
170  # test case.
171  source = None
172
173  for ((line1, lookahead1), (line2, lookahead2)) in zip_longest(
174      line_pairs(lines1), line_pairs(lines2), fillvalue=(None, None)):
175
176    # Only one of the two iterators should run out.
177    assert not (line1 is None and line2 is None)
178
179    # One iterator ends earlier.
180    if line1 is None:
181      return '+ %s' % short_line_output(line2), source
182    if line2 is None:
183      return '- %s' % short_line_output(line1), source
184
185    # If lines are equal, no further checks are necessary.
186    if line1 == line2:
187      # Instrumented original-source-file output must be equal in both
188      # versions. It only makes sense to update it here when both lines
189      # are equal.
190      if line1.startswith(ORIGINAL_SOURCE_PREFIX):
191        source = line1[len(ORIGINAL_SOURCE_PREFIX):]
192      continue
193
194    # Look ahead. If next line is a caret, ignore this line.
195    if caret_match(lookahead1, lookahead2):
196      continue
197
198    # Check if a regexp allows these lines to be different.
199    if ignore_by_regexp(line1, line2, allowed):
200      continue
201
202    # Lines are different.
203    return (
204        '- %s\n+ %s' % (short_line_output(line1), short_line_output(line2)),
205        source,
206    )
207
208  # No difference found.
209  return None, source
210
211
212def get_suppression(skip=False):
213  return V8Suppression(skip)
214
215
216class V8Suppression(object):
217  def __init__(self, skip):
218    if skip:
219      self.allowed_line_diffs = []
220      self.ignore_output = {}
221      self.ignore_sources = {}
222    else:
223      self.allowed_line_diffs = ALLOWED_LINE_DIFFS
224      self.ignore_output = IGNORE_OUTPUT
225      self.ignore_sources = IGNORE_SOURCES
226
227  def diff(self, output1, output2):
228    # Diff capped lines in the presence of crashes.
229    return self.diff_lines(
230        *map(str.splitlines, get_output_capped(output1, output2)))
231
232  def diff_lines(self, output1_lines, output2_lines):
233    return diff_output(
234        output1_lines,
235        output2_lines,
236        self.allowed_line_diffs,
237        IGNORE_LINES,
238        IGNORE_LINES,
239    )
240
241  def ignore_by_content(self, testcase):
242    # Strip off test case preamble.
243    try:
244      lines = testcase.splitlines()
245      lines = lines[lines.index(
246          'print("js-mutation: start generated test case");'):]
247      content = '\n'.join(lines)
248    except ValueError:
249      # Search the whole test case if preamble can't be found. E.g. older
250      # already minimized test cases might have dropped the delimiter line.
251      content = testcase
252    for bug, exp in IGNORE_TEST_CASES.items():
253      if exp.search(content):
254        return bug
255    return None
256
257  def ignore_by_metadata(self, metadata):
258    for bug, sources in self.ignore_sources.items():
259      for source in sources:
260        if source in metadata['sources']:
261          return bug
262    return None
263
264  def ignore_by_output(self, output):
265    def check(mapping):
266      for bug, exp in mapping.items():
267        if exp.search(output):
268          return bug
269      return None
270    bug = check(self.ignore_output)
271    if bug:
272      return bug
273    return None
274