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 30# Max line length for regular experessions checking for lines to ignore. 31MAX_LINE_LENGTH = 512 32 33# For ignoring lines before carets and to ignore caret positions. 34CARET_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. 39IGNORE_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. 82IGNORE_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. 93IGNORE_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. 113ALLOWED_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. 150IGNORE_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. 172ALLOWED_LINE_DIFFS = [re.compile(exp) for exp in ALLOWED_LINE_DIFFS] 173IGNORE_LINES = [re.compile(exp) for exp in IGNORE_LINES] 174 175ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: ' 176 177def line_pairs(lines): 178 return itertools.izip_longest( 179 lines, itertools.islice(lines, 1, None), fillvalue=None) 180 181 182def 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 191def 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 198def 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 212def 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 272def get_suppression(arch1, config1, arch2, config2): 273 return V8Suppression(arch1, config1, arch2, config2) 274 275 276class 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 293class 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