File: v8_suppressions.py

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (290 lines) | stat: -rw-r--r-- 10,056 bytes parent folder | download | duplicates (6)
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# 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 re

from itertools import zip_longest

# Max line length for regular experessions checking for lines to ignore.
MAX_LINE_LENGTH = 512

# Ignore by output pattern. Map from bug->regexp like above.
IGNORE_OUTPUT = {
  '_fake_difference_':
      re.compile(r'^.*___fake_difference___$', re.M),
}

# Avoid cross-arch comparisons if we find this string in the output.
# We also need to ignore this output in the diffs as this string might
# only be printed in the baseline run.
AVOID_CROSS_ARCH_COMPARISON_RE = (
    r'^Warning: This run cannot be compared across architectures\.$')

# Lines matching any of the following regular expressions will be ignored.
# Use uncompiled regular expressions - they'll be compiled later.
# All expressions must end with a $ without linebreak.
IGNORE_LINES = [
  r'^Warning: .+ is deprecated.*$',
  r'^Try --help for options$',
  AVOID_CROSS_ARCH_COMPARISON_RE,
]

# List of pairs (<flag string>, <regexp string>). If the regexp matches the
# test content, the flag is dropped from the command line of any run.
# If the flag is part of the third run, the entire run is dropped, as it
# in many cases would just be redundant to the second default run.
DROP_FLAGS_ON_CONTENT = [
    ('--jitless', r'\%Wasm(Struct|Array|GenerateRandomModule)\('),
]

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

# We'll match the linebreak explicitly to remove it as well.
assert all(exp.endswith('$') for exp in IGNORE_LINES)
assert not any(exp.endswith('\n$') for exp in IGNORE_LINES)
IGNORE_LINES = [exp[:-1] + '\n' for exp in IGNORE_LINES]

# Compile regular expressions.
IGNORE_LINES = [re.compile(exp.encode('utf-8'), re.M) for exp in IGNORE_LINES]

ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: '

SMOKE_TEST_SOURCE = 'foozzie smoke test'

SMOKE_TEST_END_TOKEN = '___foozzie___smoke_test_end___'

SMOKE_TEST_OUTPUT_RE = f'(.*?){SMOKE_TEST_END_TOKEN}'

SMOKE_TEST_INVERSE_OUTPUT_RE = f'.*?{SMOKE_TEST_END_TOKEN}\\s(.*)'


def get_output_capped(output1, output2):
  """Returns a pair of stdout byte arrays.

  The arrays are safely capped if at least one run has crashed.
  """

  # No length difference or no crash -> no capping.
  if (len(output1.stdout_bytes) == len(output2.stdout_bytes) or
      (not output1.HasCrashed() and not output2.HasCrashed())):
    return output1.stdout_bytes, output2.stdout_bytes

  # Both runs have crashed, cap by the shorter output.
  if output1.HasCrashed() and output2.HasCrashed():
    cap = min(len(output1.stdout_bytes), len(output2.stdout_bytes))
  # Only the first run has crashed, cap by its output length.
  elif output1.HasCrashed():
    cap = len(output1.stdout_bytes)
  # Similar if only the second run has crashed.
  else:
    cap = len(output2.stdout_bytes)

  return output1.stdout_bytes[0:cap], output2.stdout_bytes[0:cap]


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


def diff_output(lines1, lines2):
  """Returns a tuple (difference, source).

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

  The source is the last source output within the test case, or None if no
  such output existed.
  """
  # This keeps track where we are in the original source file of the fuzz
  # test case. We always start with the smoke-test part.
  source = SMOKE_TEST_SOURCE

  for line1, line2 in zip_longest(lines1, lines2, fillvalue=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:
      return f'+ {short_line_output(line2)}', source
    if line2 is None:
      return f'- {short_line_output(line1)}', source

    # Lines are equal.
    if line1 == line2:
      # 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 source == SMOKE_TEST_SOURCE and line1 == SMOKE_TEST_END_TOKEN:
        source = None
      elif line1.startswith(ORIGINAL_SOURCE_PREFIX):
        source = line1[len(ORIGINAL_SOURCE_PREFIX):]
      continue

    # Lines are different.
    short_line1 = short_line_output(line1)
    short_line2 = short_line_output(line2)
    return f'- {short_line1}\n+ {short_line2}', source

  # No difference found.
  return None, source


def get_suppression(skip=False):
  return V8Suppression(skip)


def decode(output):
  try:
    return output.decode('utf-8')
  except UnicodeDecodeError:
    return output.decode('latin-1')


def ignore_lines(text):
  result = text
  for line_re in IGNORE_LINES:
    result = line_re.sub(b'', result)
  return result


class V8Suppression(object):
  def __init__(self, skip):
    if skip:
      self.ignore_output = {}
      self.drop_flags_on_content = []
    else:
      self.ignore_output = IGNORE_OUTPUT
      self.drop_flags_on_content = DROP_FLAGS_ON_CONTENT

  def diff(self, output1, output2):
    """Diff capped lines in the presence of crashes."""

    # We remove ignored lines in the raw bytes before output capping, as such
    # a line might be present in the non-crash run. The crashy run however
    # determines the byte length and might cap the to-be-ignored line right
    # in the middle otherwise (https://crbug.com/381384408).
    output1.stdout_bytes = ignore_lines(output1.stdout_bytes)
    output2.stdout_bytes = ignore_lines(output2.stdout_bytes)

    # We need to cap raw bytes before decoding, since bytes in the capped part
    # could lead to decoding errors and an encoding change of the bytes in the
    # non-capped part (https://crbug.com/40914456).
    output1_capped, output2_capped = get_output_capped(output1, output2)

    lines1 = decode(output1_capped).splitlines()
    lines2 = decode(output2_capped).splitlines()

    return self.diff_lines(lines1, lines2)

  def diff_lines(self, output1_lines, output2_lines):
    return diff_output(output1_lines, output2_lines)

  def reduced_output(self, output, source):
    """Return output reduced by the source's origin, either only smoke-test
    output or only non-smoke-test output.
    """
    if source == SMOKE_TEST_SOURCE:
      exp = SMOKE_TEST_OUTPUT_RE
    else:
      exp = SMOKE_TEST_INVERSE_OUTPUT_RE
    match = re.search(exp, output, re.S)
    if match:
      return match.group(1)
    return output

  def ignore_by_output(self, output):
    def check(mapping):
      for bug, exp in mapping.items():
        if exp.search(output):
          return bug
      return None
    bug = check(self.ignore_output)
    if bug:
      return bug
    return None

  def _remove_config_flag(self, config, flag):
    if config.has_config_flag(flag):
      config.remove_config_flag(flag)
      return [
          f'Dropped {flag} from {config.label} config based on content rule.'
      ]
    return []

  def adjust_configs_by_content(self, execution_configs, testcase):
    """Modifies the execution configs if the testcase content matches a
    regular expression defined in DROP_FLAGS_ON_CONTENT above.

    The specified flag is dropped from the first two configs. Further
    configs are dropped entirely if the specified flag is used.

    Returns: A changelog as a list of strings.
    """
    logs = []
    assert len(execution_configs) > 1
    for flag, regexp in self.drop_flags_on_content:
      # Check first if the flag we need to drop appears anywhere. This is
      # faster than processing the content.
      if not any(config.has_config_flag(flag) for config in execution_configs):
        continue
      if not re.search(regexp, testcase):
        continue
      logs += self._remove_config_flag(execution_configs[0], flag)
      logs += self._remove_config_flag(execution_configs[1], flag)
      for config in execution_configs[2:]:
        if config.has_config_flag(flag):
          execution_configs.remove(config)
          logs.append(f'Dropped {config.label} config using '
                      f'{flag} based on content rule.')
    return logs

  def adjust_configs_by_output(self, execution_configs, output):
    """Modifies the execution configs if the baseline output contains a
    certain directive.

    Currently this only searches for one particular directive, after which
    we ensure to compare on the same architecture.

    Returns: A changelog as a list of strings.
    """
    # Cross-arch configs have a same-arch fallback. Check first if there
    # are any, as this check is very cheap.
    logs = []
    if not any(config.fallback for config in execution_configs):
      return []
    if not re.search(AVOID_CROSS_ARCH_COMPARISON_RE, output, re.M):
      return []
    for i, config in enumerate(execution_configs):
      if config.fallback:
        logs.append(
            f'Running the {config.label} config on the same architecture.')
        execution_configs[i] = config.fallback
    return logs