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