v8_suppressions.py 10.2 KB
Newer Older
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
# Copyright 2016 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""
Suppressions for V8 correctness fuzzer failures.

We support three types of suppressions:
1. Ignore test case by pattern.
Map a regular expression to a bug entry. A new failure will be reported
when the pattern matches a JS test case.
Subsequent matches will be recoreded under the first failure.

2. Ignore test run by output pattern:
Map a regular expression to a bug entry. A new failure will be reported
when the pattern matches the output of a particular run.
Subsequent matches will be recoreded under the first failure.

3. Relax line-to-line comparisons with expressions of lines to ignore and
lines to be normalized (i.e. ignore only portions of lines).
These are not tied to bugs, be careful to not silently switch off this tool!

Alternatively, think about adding a behavior change to v8_suppressions.js
to silence a particular class of problems.
"""

import itertools
import re

30 31 32 33 34 35 36
try:
  # Python 3
  from itertools import zip_longest
except ImportError:
  # Python 2
  from itertools import izip_longest as zip_longest

37 38 39 40 41 42
# Max line length for regular experessions checking for lines to ignore.
MAX_LINE_LENGTH = 512

# For ignoring lines before carets and to ignore caret positions.
CARET_RE = re.compile(r'^\s*\^\s*$')

43 44
# Ignore by original source files. Map from bug->list of relative file paths in
# V8, e.g. '/v8/test/mjsunit/d8-performance-now.js' including /v8/. A test will
45 46 47 48
# be suppressed if one of the files below was used to mutate the test.
IGNORE_SOURCES = {
}

49 50 51 52 53
# Ignore by test case pattern. Map from config->bug->regexp. Config '' is used
# to match all configurations. Otherwise use either a compiler configuration,
# e.g. ignition or validate_asm or an architecture, e.g. x64 or ia32.
# Bug is preferred to be a crbug.com/XYZ, but can be any short distinguishable
# label.
54
# Regular expressions are assumed to be compiled. We use regexp.search.
55 56 57
IGNORE_TEST_CASES = {
}

58 59
# Ignore by output pattern. Map from config->bug->regexp. See IGNORE_TEST_CASES
# on how to specify config keys.
60 61 62 63 64
# Bug is preferred to be a crbug.com/XYZ, but can be any short distinguishable
# label.
# Regular expressions are assumed to be compiled. We use regexp.search.
IGNORE_OUTPUT = {
  '': {
65 66
    'crbug.com/689877':
        re.compile(r'^.*SyntaxError: .*Stack overflow$', re.M),
67 68 69 70 71 72 73 74 75 76 77 78 79 80
  },
}

# Lines matching any of the following regular expressions will be ignored
# if appearing on both sides. The capturing groups need to match exactly.
# Use uncompiled regular expressions - they'll be compiled later.
ALLOWED_LINE_DIFFS = [
  # Ignore caret position in stack traces.
  r'^\s*\^\s*$',

  # Ignore some stack trace headers as messages might not match.
  r'^(.*)TypeError: .* is not a function$',
  r'^(.*)TypeError: .* is not a constructor$',
  r'^(.*)TypeError: (.*) is not .*$',
81
  r'^(.*):\d+: TypeError: Message suppressed for fuzzers.*$',
82 83 84 85 86 87 88 89 90 91 92 93
  r'^(.*)ReferenceError: .* is not defined$',
  r'^(.*):\d+: ReferenceError: .* is not defined$',

  # These are rarely needed. It includes some cases above.
  r'^\w*Error: .* is not .*$',
  r'^(.*) \w*Error: .* is not .*$',
  r'^(.*):\d+: \w*Error: .* is not .*$',

  # Some test cases just print the message.
  r'^.* is not a function(.*)$',
  r'^(.*) is not a .*$',

94 95
  # crbug.com/680064. This subsumes one of the above expressions.
  r'^(.*)TypeError: .* function$',
96 97 98 99 100 101 102 103
]

# Lines matching any of the following regular expressions will be ignored.
# Use uncompiled regular expressions - they'll be compiled later.
IGNORE_LINES = [
  r'^Warning: unknown flag .*$',
  r'^Warning: .+ is deprecated.*$',
  r'^Try --help for options$',
104

105 106
  # crbug.com/705962
  r'^\s\[0x[0-9a-f]+\]$',
107 108 109 110 111 112 113 114 115 116
]


###############################################################################
# Implementation - you should not need to change anything below this point.

# Compile regular expressions.
ALLOWED_LINE_DIFFS = [re.compile(exp) for exp in ALLOWED_LINE_DIFFS]
IGNORE_LINES = [re.compile(exp) for exp in IGNORE_LINES]

117
ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: '
118

119

120 121
def get_output_capped(output1, output2):
  """Returns a pair of stdout strings.
122

123
  The strings are safely capped if at least one run has crashed.
124 125 126
  """

  # No length difference or no crash -> no capping.
127
  if (len(output1.stdout) == len(output2.stdout) or
128
      (not output1.HasCrashed() and not output2.HasCrashed())):
129
    return output1.stdout, output2.stdout
130 131 132

  # Both runs have crashed, cap by the shorter output.
  if output1.HasCrashed() and output2.HasCrashed():
133
    cap = min(len(output1.stdout), len(output2.stdout))
134 135
  # Only the first run has crashed, cap by its output length.
  elif output1.HasCrashed():
136
    cap = len(output1.stdout)
137 138
  # Similar if only the second run has crashed.
  else:
139
    cap = len(output2.stdout)
140

141
  return output1.stdout[0:cap], output2.stdout[0:cap]
142 143


144
def line_pairs(lines):
145
  return zip_longest(
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
      lines, itertools.islice(lines, 1, None), fillvalue=None)


def caret_match(line1, line2):
  if (not line1 or
      not line2 or
      len(line1) > MAX_LINE_LENGTH or
      len(line2) > MAX_LINE_LENGTH):
    return False
  return bool(CARET_RE.match(line1) and CARET_RE.match(line2))


def short_line_output(line):
  if len(line) <= MAX_LINE_LENGTH:
    # Avoid copying.
    return line
  return line[0:MAX_LINE_LENGTH] + '...'


def ignore_by_regexp(line1, line2, allowed):
  if len(line1) > MAX_LINE_LENGTH or len(line2) > MAX_LINE_LENGTH:
    return False
  for exp in allowed:
    match1 = exp.match(line1)
    match2 = exp.match(line2)
    if match1 and match2:
      # If there are groups in the regexp, ensure the groups matched the same
      # things.
      if match1.groups() == match2.groups():  # tuple comparison
        return True
  return False


def diff_output(output1, output2, allowed, ignore1, ignore2):
180
  """Returns a tuple (difference, source).
181 182 183 184

  The difference is None if there's no difference, otherwise a string
  with a readable diff.

185 186
  The source is the last source output within the test case, or None if no
  such output existed.
187
  """
188 189 190 191 192
  def useful_line(ignore):
    def fun(line):
      return all(not e.match(line) for e in ignore)
    return fun

193 194
  lines1 = list(filter(useful_line(ignore1), output1))
  lines2 = list(filter(useful_line(ignore2), output2))
195

196 197
  # This keeps track where we are in the original source file of the fuzz
  # test case.
198
  source = None
199

200
  for ((line1, lookahead1), (line2, lookahead2)) in zip_longest(
201 202 203 204 205 206 207
      line_pairs(lines1), line_pairs(lines2), fillvalue=(None, None)):

    # Only one of the two iterators should run out.
    assert not (line1 is None and line2 is None)

    # One iterator ends earlier.
    if line1 is None:
208
      return '+ %s' % short_line_output(line2), source
209
    if line2 is None:
210
      return '- %s' % short_line_output(line1), source
211 212 213

    # If lines are equal, no further checks are necessary.
    if line1 == line2:
214 215 216 217 218
      # Instrumented original-source-file output must be equal in both
      # versions. It only makes sense to update it here when both lines
      # are equal.
      if line1.startswith(ORIGINAL_SOURCE_PREFIX):
        source = line1[len(ORIGINAL_SOURCE_PREFIX):]
219 220 221 222 223 224 225 226 227 228 229
      continue

    # Look ahead. If next line is a caret, ignore this line.
    if caret_match(lookahead1, lookahead2):
      continue

    # Check if a regexp allows these lines to be different.
    if ignore_by_regexp(line1, line2, allowed):
      continue

    # Lines are different.
230 231
    return (
        '- %s\n+ %s' % (short_line_output(line1), short_line_output(line2)),
232
        source,
233
    )
234 235

  # No difference found.
236
  return None, source
237 238


239 240
def get_suppression(arch1, config1, arch2, config2, skip=False):
  return V8Suppression(arch1, config1, arch2, config2, skip)
241 242 243 244 245 246


class Suppression(object):
  def diff(self, output1, output2):
    return None

247
  def ignore_by_metadata(self, metadata):
248
    return None
249 250

  def ignore_by_content(self, testcase):
251
    return None
252 253

  def ignore_by_output1(self, output):
254
    return None
255 256

  def ignore_by_output2(self, output):
257
    return None
258 259 260


class V8Suppression(Suppression):
261
  def __init__(self, arch1, config1, arch2, config2, skip):
262 263 264 265
    self.arch1 = arch1
    self.config1 = config1
    self.arch2 = arch2
    self.config2 = config2
266 267 268 269 270 271 272 273
    if skip:
      self.allowed_line_diffs = []
      self.ignore_output = {}
      self.ignore_sources = {}
    else:
      self.allowed_line_diffs = ALLOWED_LINE_DIFFS
      self.ignore_output = IGNORE_OUTPUT
      self.ignore_sources = IGNORE_SOURCES
274 275

  def diff(self, output1, output2):
276
    # Diff capped lines in the presence of crashes.
277 278
    return self.diff_lines(
        *map(str.splitlines, get_output_capped(output1, output2)))
279 280

  def diff_lines(self, output1_lines, output2_lines):
281
    return diff_output(
282 283
        output1_lines,
        output2_lines,
284
        self.allowed_line_diffs,
285 286 287 288
        IGNORE_LINES,
        IGNORE_LINES,
    )

289
  def ignore_by_content(self, testcase):
290
    # Strip off test case preamble.
291 292
    try:
      lines = testcase.splitlines()
293 294
      lines = lines[lines.index(
          'print("js-mutation: start generated test case");'):]
295 296 297 298 299
      content = '\n'.join(lines)
    except ValueError:
      # Search the whole test case if preamble can't be found. E.g. older
      # already minimized test cases might have dropped the delimiter line.
      content = testcase
300
    for key in ['', self.arch1, self.arch2, self.config1, self.config2]:
301
      for bug, exp in IGNORE_TEST_CASES.get(key, {}).items():
302 303 304
        if exp.search(content):
          return bug
    return None
305

306
  def ignore_by_metadata(self, metadata):
307
    for bug, sources in self.ignore_sources.items():
308 309 310
      for source in sources:
        if source in metadata['sources']:
          return bug
311
    return None
312

313 314 315 316 317 318 319 320
  def ignore_by_output1(self, output):
    return self.ignore_by_output(output, self.arch1, self.config1)

  def ignore_by_output2(self, output):
    return self.ignore_by_output(output, self.arch2, self.config2)

  def ignore_by_output(self, output, arch, config):
    def check(mapping):
321
      for bug, exp in mapping.items():
322 323 324
        if exp.search(output):
          return bug
      return None
325
    for key in ['', arch, config]:
326
      bug = check(self.ignore_output.get(key, {}))
327 328
      if bug:
        return bug
329
    return None