Home | History | Annotate | Download | only in foozzie
      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 """
      6 Suppressions for V8 correctness fuzzer failures.
      7 
      8 We support three types of suppressions:
      9 1. Ignore test case by pattern.
     10 Map a regular expression to a bug entry. A new failure will be reported
     11 when the pattern matches a JS test case.
     12 Subsequent matches will be recoreded under the first failure.
     13 
     14 2. Ignore test run by output pattern:
     15 Map a regular expression to a bug entry. A new failure will be reported
     16 when the pattern matches the output of a particular run.
     17 Subsequent matches will be recoreded under the first failure.
     18 
     19 3. Relax line-to-line comparisons with expressions of lines to ignore and
     20 lines to be normalized (i.e. ignore only portions of lines).
     21 These are not tied to bugs, be careful to not silently switch off this tool!
     22 
     23 Alternatively, think about adding a behavior change to v8_suppressions.js
     24 to silence a particular class of problems.
     25 """
     26 
     27 import itertools
     28 import re
     29 
     30 # Max line length for regular experessions checking for lines to ignore.
     31 MAX_LINE_LENGTH = 512
     32 
     33 # For ignoring lines before carets and to ignore caret positions.
     34 CARET_RE = re.compile(r'^\s*\^\s*$')
     35 
     36 # Ignore by original source files. Map from bug->list of relative file paths in
     37 # V8, e.g. '/v8/test/mjsunit/d8-performance-now.js' including /v8/. A test will
     38 # be suppressed if one of the files below was used to mutate the test.
     39 IGNORE_SOURCES = {
     40   # This contains a usage of f.arguments that often fires.
     41   'crbug.com/662424': [
     42     '/v8/test/mjsunit/bugs/bug-222.js',
     43     '/v8/test/mjsunit/bugs/bug-941049.js',
     44     '/v8/test/mjsunit/regress/regress-crbug-668795.js',
     45     '/v8/test/mjsunit/regress/regress-2989.js',
     46   ],
     47 
     48   'crbug.com/681088': [
     49     '/v8/test/mjsunit/asm/asm-validation.js',
     50     '/v8/test/mjsunit/asm/b5528-comma.js',
     51     '/v8/test/mjsunit/asm/pointer-masking.js',
     52     '/v8/test/mjsunit/compiler/regress-443744.js',
     53     '/v8/test/mjsunit/regress/regress-599719.js',
     54     '/v8/test/mjsunit/regress/wasm/regression-647649.js',
     55     '/v8/test/mjsunit/wasm/asm-wasm.js',
     56     '/v8/test/mjsunit/wasm/asm-wasm-deopt.js',
     57     '/v8/test/mjsunit/wasm/asm-wasm-heap.js',
     58     '/v8/test/mjsunit/wasm/asm-wasm-literals.js',
     59     '/v8/test/mjsunit/wasm/asm-wasm-stack.js',
     60   ],
     61 
     62   'crbug.com/681241': [
     63     '/v8/test/mjsunit/regress/regress-617526.js',
     64     '/v8/test/mjsunit/regress/wasm/regression-02862.js',
     65   ],
     66 
     67   'crbug.com/688159': [
     68     '/v8/test/mjsunit/es7/exponentiation-operator.js',
     69   ],
     70 }
     71 
     72 # Ignore by test case pattern. Map from bug->regexp.
     73 # Regular expressions are assumed to be compiled. We use regexp.match.
     74 # Make sure the code doesn't match in the preamble portion of the test case
     75 # (i.e. in the modified inlined mjsunit.js). You can reference the comment
     76 # between the two parts like so:
     77 #  'crbug.com/666308':
     78 #      re.compile(r'.*End stripped down and modified version.*'
     79 #                 r'\.prototype.*instanceof.*.*', re.S)
     80 # TODO(machenbach): Insert a JS sentinel between the two parts, because
     81 # comments are stripped during minimization.
     82 IGNORE_TEST_CASES = {
     83 }
     84 
     85 # Ignore by output pattern. Map from config->bug->regexp. Config '' is used
     86 # to match all configurations. Otherwise use either a compiler configuration,
     87 # e.g. fullcode or validate_asm or an architecture, e.g. x64 or ia32 or a
     88 # comma-separated combination, e.g. x64,fullcode, for more specific
     89 # suppressions.
     90 # Bug is preferred to be a crbug.com/XYZ, but can be any short distinguishable
     91 # label.
     92 # Regular expressions are assumed to be compiled. We use regexp.search.
     93 IGNORE_OUTPUT = {
     94   '': {
     95     'crbug.com/664068':
     96         re.compile(r'RangeError(?!: byte length)', re.S),
     97     'crbug.com/667678':
     98         re.compile(r'\[native code\]', re.S),
     99     'crbug.com/681806':
    100         re.compile(r'WebAssembly\.Instance', re.S),
    101     'crbug.com/681088':
    102         re.compile(r'TypeError: Cannot read property \w+ of undefined', re.S),
    103   },
    104   'validate_asm': {
    105     'validate_asm':
    106         re.compile(r'TypeError'),
    107   },
    108 }
    109 
    110 # Lines matching any of the following regular expressions will be ignored
    111 # if appearing on both sides. The capturing groups need to match exactly.
    112 # Use uncompiled regular expressions - they'll be compiled later.
    113 ALLOWED_LINE_DIFFS = [
    114   # Ignore caret position in stack traces.
    115   r'^\s*\^\s*$',
    116 
    117   # Ignore some stack trace headers as messages might not match.
    118   r'^(.*)TypeError: .* is not a function$',
    119   r'^(.*)TypeError: .* is not a constructor$',
    120   r'^(.*)TypeError: (.*) is not .*$',
    121   r'^(.*)ReferenceError: .* is not defined$',
    122   r'^(.*):\d+: ReferenceError: .* is not defined$',
    123 
    124   # These are rarely needed. It includes some cases above.
    125   r'^\w*Error: .* is not .*$',
    126   r'^(.*) \w*Error: .* is not .*$',
    127   r'^(.*):\d+: \w*Error: .* is not .*$',
    128 
    129   # Some test cases just print the message.
    130   r'^.* is not a function(.*)$',
    131   r'^(.*) is not a .*$',
    132 
    133   # Ignore lines of stack traces as character positions might not match.
    134   r'^    at (?:new )?([^:]*):\d+:\d+(.*)$',
    135   r'^(.*):\d+:(.*)$',
    136 
    137   # crbug.com/662840
    138   r"^.*(?:Trying to access ')?(\w*)(?:(?:' through proxy)|"
    139   r"(?: is not defined))$",
    140 
    141   # crbug.com/680064. This subsumes one of the above expressions.
    142   r'^(.*)TypeError: .* function$',
    143 
    144   # crbug.com/681326
    145   r'^(.*<anonymous>):\d+:\d+(.*)$',
    146 ]
    147 
    148 # Lines matching any of the following regular expressions will be ignored.
    149 # Use uncompiled regular expressions - they'll be compiled later.
    150 IGNORE_LINES = [
    151   r'^Validation of asm\.js module failed: .+$',
    152   r'^.*:\d+: Invalid asm.js: .*$',
    153   r'^Warning: unknown flag .*$',
    154   r'^Warning: .+ is deprecated.*$',
    155   r'^Try --help for options$',
    156 
    157   # crbug.com/677032
    158   r'^.*:\d+:.*asm\.js.*: success$',
    159 
    160   # crbug.com/680064
    161   r'^\s*at .* \(<anonymous>\)$',
    162 
    163   # crbug.com/689877
    164   r'^.*SyntaxError: .*Stack overflow$',
    165 ]
    166 
    167 
    168 ###############################################################################
    169 # Implementation - you should not need to change anything below this point.
    170 
    171 # Compile regular expressions.
    172 ALLOWED_LINE_DIFFS = [re.compile(exp) for exp in ALLOWED_LINE_DIFFS]
    173 IGNORE_LINES = [re.compile(exp) for exp in IGNORE_LINES]
    174 
    175 ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: '
    176 
    177 def line_pairs(lines):
    178   return itertools.izip_longest(
    179       lines, itertools.islice(lines, 1, None), fillvalue=None)
    180 
    181 
    182 def caret_match(line1, line2):
    183   if (not line1 or
    184       not line2 or
    185       len(line1) > MAX_LINE_LENGTH or
    186       len(line2) > MAX_LINE_LENGTH):
    187     return False
    188   return bool(CARET_RE.match(line1) and CARET_RE.match(line2))
    189 
    190 
    191 def short_line_output(line):
    192   if len(line) <= MAX_LINE_LENGTH:
    193     # Avoid copying.
    194     return line
    195   return line[0:MAX_LINE_LENGTH] + '...'
    196 
    197 
    198 def ignore_by_regexp(line1, line2, allowed):
    199   if len(line1) > MAX_LINE_LENGTH or len(line2) > MAX_LINE_LENGTH:
    200     return False
    201   for exp in allowed:
    202     match1 = exp.match(line1)
    203     match2 = exp.match(line2)
    204     if match1 and match2:
    205       # If there are groups in the regexp, ensure the groups matched the same
    206       # things.
    207       if match1.groups() == match2.groups():  # tuple comparison
    208         return True
    209   return False
    210 
    211 
    212 def diff_output(output1, output2, allowed, ignore1, ignore2):
    213   """Returns a tuple (difference, source).
    214 
    215   The difference is None if there's no difference, otherwise a string
    216   with a readable diff.
    217 
    218   The source is the last source output within the test case, or None if no
    219   such output existed.
    220   """
    221   def useful_line(ignore):
    222     def fun(line):
    223       return all(not e.match(line) for e in ignore)
    224     return fun
    225 
    226   lines1 = filter(useful_line(ignore1), output1)
    227   lines2 = filter(useful_line(ignore2), output2)
    228 
    229   # This keeps track where we are in the original source file of the fuzz
    230   # test case.
    231   source = None
    232 
    233   for ((line1, lookahead1), (line2, lookahead2)) in itertools.izip_longest(
    234       line_pairs(lines1), line_pairs(lines2), fillvalue=(None, None)):
    235 
    236     # Only one of the two iterators should run out.
    237     assert not (line1 is None and line2 is None)
    238 
    239     # One iterator ends earlier.
    240     if line1 is None:
    241       return '+ %s' % short_line_output(line2), source
    242     if line2 is None:
    243       return '- %s' % short_line_output(line1), source
    244 
    245     # If lines are equal, no further checks are necessary.
    246     if line1 == line2:
    247       # Instrumented original-source-file output must be equal in both
    248       # versions. It only makes sense to update it here when both lines
    249       # are equal.
    250       if line1.startswith(ORIGINAL_SOURCE_PREFIX):
    251         source = line1[len(ORIGINAL_SOURCE_PREFIX):]
    252       continue
    253 
    254     # Look ahead. If next line is a caret, ignore this line.
    255     if caret_match(lookahead1, lookahead2):
    256       continue
    257 
    258     # Check if a regexp allows these lines to be different.
    259     if ignore_by_regexp(line1, line2, allowed):
    260       continue
    261 
    262     # Lines are different.
    263     return (
    264         '- %s\n+ %s' % (short_line_output(line1), short_line_output(line2)),
    265         source,
    266     )
    267 
    268   # No difference found.
    269   return None, source
    270 
    271 
    272 def get_suppression(arch1, config1, arch2, config2):
    273   return V8Suppression(arch1, config1, arch2, config2)
    274 
    275 
    276 class Suppression(object):
    277   def diff(self, output1, output2):
    278     return None
    279 
    280   def ignore_by_metadata(self, metadata):
    281     return False
    282 
    283   def ignore_by_content(self, testcase):
    284     return False
    285 
    286   def ignore_by_output1(self, output):
    287     return False
    288 
    289   def ignore_by_output2(self, output):
    290     return False
    291 
    292 
    293 class V8Suppression(Suppression):
    294   def __init__(self, arch1, config1, arch2, config2):
    295     self.arch1 = arch1
    296     self.config1 = config1
    297     self.arch2 = arch2
    298     self.config2 = config2
    299 
    300   def diff(self, output1, output2):
    301     return diff_output(
    302         output1.splitlines(),
    303         output2.splitlines(),
    304         ALLOWED_LINE_DIFFS,
    305         IGNORE_LINES,
    306         IGNORE_LINES,
    307     )
    308 
    309   def ignore_by_content(self, testcase):
    310     for bug, exp in IGNORE_TEST_CASES.iteritems():
    311       if exp.match(testcase):
    312         return bug
    313     return False
    314 
    315   def ignore_by_metadata(self, metadata):
    316     for bug, sources in IGNORE_SOURCES.iteritems():
    317       for source in sources:
    318         if source in metadata['sources']:
    319           return bug
    320     return False
    321 
    322   def ignore_by_output1(self, output):
    323     return self.ignore_by_output(output, self.arch1, self.config1)
    324 
    325   def ignore_by_output2(self, output):
    326     return self.ignore_by_output(output, self.arch2, self.config2)
    327 
    328   def ignore_by_output(self, output, arch, config):
    329     def check(mapping):
    330       for bug, exp in mapping.iteritems():
    331         if exp.search(output):
    332           return bug
    333       return None
    334     bug = check(IGNORE_OUTPUT.get('', {}))
    335     if bug:
    336       return bug
    337     bug = check(IGNORE_OUTPUT.get(arch, {}))
    338     if bug:
    339       return bug
    340     bug = check(IGNORE_OUTPUT.get(config, {}))
    341     if bug:
    342       return bug
    343     bug = check(IGNORE_OUTPUT.get('%s,%s' % (arch, config), {}))
    344     if bug:
    345       return bug
    346     return None
    347