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