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}
61
62# Lines matching any of the following regular expressions will be ignored
63# if appearing on both sides. The capturing groups need to match exactly.
64# Use uncompiled regular expressions - they'll be compiled later.
65ALLOWED_LINE_DIFFS = [
66  # Ignore caret position in stack traces.
67  r'^\s*\^\s*$',
68]
69
70# Lines matching any of the following regular expressions will be ignored.
71# Use uncompiled regular expressions - they'll be compiled later.
72IGNORE_LINES = [
73  r'^Warning: unknown flag .*$',
74  r'^Warning: .+ is deprecated.*$',
75  r'^Try --help for options$',
76
77  # crbug.com/705962
78  r'^\s\[0x[0-9a-f]+\]$',
79]
80
81
82###############################################################################
83# Implementation - you should not need to change anything below this point.
84
85# Compile regular expressions.
86ALLOWED_LINE_DIFFS = [re.compile(exp) for exp in ALLOWED_LINE_DIFFS]
87IGNORE_LINES = [re.compile(exp) for exp in IGNORE_LINES]
88
89ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: '
90
91
92def get_output_capped(output1, output2):
93  """Returns a pair of stdout strings.
94
95  The strings are safely capped if at least one run has crashed.
96  """
97
98  # No length difference or no crash -> no capping.
99  if (len(output1.stdout) == len(output2.stdout) or
100      (not output1.HasCrashed() and not output2.HasCrashed())):
101    return output1.stdout, output2.stdout
102
103  # Both runs have crashed, cap by the shorter output.
104  if output1.HasCrashed() and output2.HasCrashed():
105    cap = min(len(output1.stdout), len(output2.stdout))
106  # Only the first run has crashed, cap by its output length.
107  elif output1.HasCrashed():
108    cap = len(output1.stdout)
109  # Similar if only the second run has crashed.
110  else:
111    cap = len(output2.stdout)
112
113  return output1.stdout[0:cap], output2.stdout[0:cap]
114
115
116def line_pairs(lines):
117  return zip_longest(
118      lines, itertools.islice(lines, 1, None), fillvalue=None)
119
120
121def caret_match(line1, line2):
122  if (not line1 or
123      not line2 or
124      len(line1) > MAX_LINE_LENGTH or
125      len(line2) > MAX_LINE_LENGTH):
126    return False
127  return bool(CARET_RE.match(line1) and CARET_RE.match(line2))
128
129
130def short_line_output(line):
131  if len(line) <= MAX_LINE_LENGTH:
132    # Avoid copying.
133    return line
134  return line[0:MAX_LINE_LENGTH] + '...'
135
136
137def ignore_by_regexp(line1, line2, allowed):
138  if len(line1) > MAX_LINE_LENGTH or len(line2) > MAX_LINE_LENGTH:
139    return False
140  for exp in allowed:
141    match1 = exp.match(line1)
142    match2 = exp.match(line2)
143    if match1 and match2:
144      # If there are groups in the regexp, ensure the groups matched the same
145      # things.
146      if match1.groups() == match2.groups():  # tuple comparison
147        return True
148  return False
149
150
151def diff_output(output1, output2, allowed, ignore1, ignore2):
152  """Returns a tuple (difference, source).
153
154  The difference is None if there's no difference, otherwise a string
155  with a readable diff.
156
157  The source is the last source output within the test case, or None if no
158  such output existed.
159  """
160  def useful_line(ignore):
161    def fun(line):
162      return all(not e.match(line) for e in ignore)
163    return fun
164
165  lines1 = list(filter(useful_line(ignore1), output1))
166  lines2 = list(filter(useful_line(ignore2), output2))
167
168  # This keeps track where we are in the original source file of the fuzz
169  # test case.
170  source = None
171
172  for ((line1, lookahead1), (line2, lookahead2)) in zip_longest(
173      line_pairs(lines1), line_pairs(lines2), fillvalue=(None, None)):
174
175    # Only one of the two iterators should run out.
176    assert not (line1 is None and line2 is None)
177
178    # One iterator ends earlier.
179    if line1 is None:
180      return '+ %s' % short_line_output(line2), source
181    if line2 is None:
182      return '- %s' % short_line_output(line1), source
183
184    # If lines are equal, no further checks are necessary.
185    if line1 == line2:
186      # Instrumented original-source-file output must be equal in both
187      # versions. It only makes sense to update it here when both lines
188      # are equal.
189      if line1.startswith(ORIGINAL_SOURCE_PREFIX):
190        source = line1[len(ORIGINAL_SOURCE_PREFIX):]
191      continue
192
193    # Look ahead. If next line is a caret, ignore this line.
194    if caret_match(lookahead1, lookahead2):
195      continue
196
197    # Check if a regexp allows these lines to be different.
198    if ignore_by_regexp(line1, line2, allowed):
199      continue
200
201    # Lines are different.
202    return (
203        '- %s\n+ %s' % (short_line_output(line1), short_line_output(line2)),
204        source,
205    )
206
207  # No difference found.
208  return None, source
209
210
211def get_suppression(skip=False):
212  return V8Suppression(skip)
213
214
215class V8Suppression(object):
216  def __init__(self, skip):
217    if skip:
218      self.allowed_line_diffs = []
219      self.ignore_output = {}
220      self.ignore_sources = {}
221    else:
222      self.allowed_line_diffs = ALLOWED_LINE_DIFFS
223      self.ignore_output = IGNORE_OUTPUT
224      self.ignore_sources = IGNORE_SOURCES
225
226  def diff(self, output1, output2):
227    # Diff capped lines in the presence of crashes.
228    return self.diff_lines(
229        *map(str.splitlines, get_output_capped(output1, output2)))
230
231  def diff_lines(self, output1_lines, output2_lines):
232    return diff_output(
233        output1_lines,
234        output2_lines,
235        self.allowed_line_diffs,
236        IGNORE_LINES,
237        IGNORE_LINES,
238    )
239
240  def ignore_by_content(self, testcase):
241    # Strip off test case preamble.
242    try:
243      lines = testcase.splitlines()
244      lines = lines[lines.index(
245          'print("js-mutation: start generated test case");'):]
246      content = '\n'.join(lines)
247    except ValueError:
248      # Search the whole test case if preamble can't be found. E.g. older
249      # already minimized test cases might have dropped the delimiter line.
250      content = testcase
251    for bug, exp in IGNORE_TEST_CASES.items():
252      if exp.search(content):
253        return bug
254    return None
255
256  def ignore_by_metadata(self, metadata):
257    for bug, sources in self.ignore_sources.items():
258      for source in sources:
259        if source in metadata['sources']:
260          return bug
261    return None
262
263  def ignore_by_output(self, output):
264    def check(mapping):
265      for bug, exp in mapping.items():
266        if exp.search(output):
267          return bug
268      return None
269    bug = check(self.ignore_output)
270    if bug:
271      return bug
272    return None
273