1019 lines
34 KiB
Python
Executable File
1019 lines
34 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Copyright (c) 2012 The Chromium 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.py
|
|
|
|
"""Post-process Valgrind suppression matcher.
|
|
|
|
Suppressions are defined as follows:
|
|
|
|
# optional one-line comments anywhere in the suppressions file.
|
|
{
|
|
<Short description of the error>
|
|
Toolname:Errortype
|
|
fun:function_name
|
|
obj:object_filename
|
|
fun:wildcarded_fun*_name
|
|
# an ellipsis wildcards zero or more functions in a stack.
|
|
...
|
|
fun:some_other_function_name
|
|
}
|
|
|
|
If ran from the command line, suppressions.py does a self-test
|
|
of the Suppression class.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
|
|
'..', 'python', 'google'))
|
|
import path_utils
|
|
|
|
|
|
ELLIPSIS = '...'
|
|
|
|
|
|
def GetSuppressions():
|
|
suppressions_root = path_utils.ScriptDir()
|
|
JOIN = os.path.join
|
|
|
|
result = {}
|
|
|
|
supp_filename = JOIN(suppressions_root, "memcheck", "suppressions.txt")
|
|
vg_common = ReadSuppressionsFromFile(supp_filename)
|
|
supp_filename = JOIN(suppressions_root, "tsan", "suppressions.txt")
|
|
tsan_common = ReadSuppressionsFromFile(supp_filename)
|
|
result['common_suppressions'] = vg_common + tsan_common
|
|
|
|
supp_filename = JOIN(suppressions_root, "memcheck", "suppressions_linux.txt")
|
|
vg_linux = ReadSuppressionsFromFile(supp_filename)
|
|
supp_filename = JOIN(suppressions_root, "tsan", "suppressions_linux.txt")
|
|
tsan_linux = ReadSuppressionsFromFile(supp_filename)
|
|
result['linux_suppressions'] = vg_linux + tsan_linux
|
|
|
|
supp_filename = JOIN(suppressions_root, "memcheck", "suppressions_mac.txt")
|
|
vg_mac = ReadSuppressionsFromFile(supp_filename)
|
|
supp_filename = JOIN(suppressions_root, "tsan", "suppressions_mac.txt")
|
|
tsan_mac = ReadSuppressionsFromFile(supp_filename)
|
|
result['mac_suppressions'] = vg_mac + tsan_mac
|
|
|
|
supp_filename = JOIN(suppressions_root, "tsan", "suppressions_win32.txt")
|
|
tsan_win = ReadSuppressionsFromFile(supp_filename)
|
|
result['win_suppressions'] = tsan_win
|
|
|
|
supp_filename = JOIN(suppressions_root, "..", "heapcheck", "suppressions.txt")
|
|
result['heapcheck_suppressions'] = ReadSuppressionsFromFile(supp_filename)
|
|
|
|
supp_filename = JOIN(suppressions_root, "drmemory", "suppressions.txt")
|
|
result['drmem_suppressions'] = ReadSuppressionsFromFile(supp_filename)
|
|
supp_filename = JOIN(suppressions_root, "drmemory", "suppressions_full.txt")
|
|
result['drmem_full_suppressions'] = ReadSuppressionsFromFile(supp_filename)
|
|
|
|
return result
|
|
|
|
|
|
def GlobToRegex(glob_pattern, ignore_case=False):
|
|
"""Translate glob wildcards (*?) into regex syntax. Escape the rest."""
|
|
regex = ''
|
|
for char in glob_pattern:
|
|
if char == '*':
|
|
regex += '.*'
|
|
elif char == '?':
|
|
regex += '.'
|
|
elif ignore_case and char.isalpha():
|
|
regex += '[%s%s]' % (char.lower(), char.upper())
|
|
else:
|
|
regex += re.escape(char)
|
|
return ''.join(regex)
|
|
|
|
|
|
def StripAndSkipCommentsIterator(lines):
|
|
"""Generator of (line_no, line) pairs that strips comments and whitespace."""
|
|
for (line_no, line) in enumerate(lines):
|
|
line = line.strip() # Drop \n
|
|
if line.startswith('#'):
|
|
continue # Comments
|
|
# Skip comment lines, but not empty lines, they indicate the end of a
|
|
# suppression. Add one to the line number as well, since most editors use
|
|
# 1-based numberings, and enumerate is 0-based.
|
|
yield (line_no + 1, line)
|
|
|
|
|
|
class Suppression(object):
|
|
"""This class represents a single stack trace suppression.
|
|
|
|
Attributes:
|
|
description: A string representing the error description.
|
|
type: A string representing the error type, e.g. Memcheck:Leak.
|
|
stack: The lines comprising the stack trace for the suppression.
|
|
regex: The actual regex used to match against scraped reports.
|
|
"""
|
|
|
|
def __init__(self, description, type, stack, defined_at, regex):
|
|
"""Inits Suppression.
|
|
|
|
description, type, stack, regex: same as class attributes
|
|
defined_at: file:line identifying where the suppression was defined
|
|
"""
|
|
self.description = description
|
|
self.type = type
|
|
self.stack = stack
|
|
self.defined_at = defined_at
|
|
self.regex = re.compile(regex, re.MULTILINE)
|
|
|
|
def Match(self, suppression_from_report):
|
|
"""Returns bool indicating whether this suppression matches
|
|
the suppression generated from Valgrind error report.
|
|
|
|
We match our suppressions against generated suppressions
|
|
(not against reports) since they have the same format
|
|
while the reports are taken from XML, contain filenames,
|
|
they are demangled, and are generally more difficult to
|
|
parse.
|
|
|
|
Args:
|
|
suppression_from_report: list of strings (function names).
|
|
Returns:
|
|
True if the suppression is not empty and matches the report.
|
|
"""
|
|
if not self.stack:
|
|
return False
|
|
lines = [f.strip() for f in suppression_from_report]
|
|
return self.regex.match('\n'.join(lines) + '\n') is not None
|
|
|
|
|
|
def FilenameToTool(filename):
|
|
"""Return the name of the tool that a file is related to, or None.
|
|
|
|
Example mappings:
|
|
tools/heapcheck/suppressions.txt -> heapcheck
|
|
tools/valgrind/tsan/suppressions.txt -> tsan
|
|
tools/valgrind/drmemory/suppressions.txt -> drmemory
|
|
tools/valgrind/drmemory/suppressions_full.txt -> drmemory
|
|
tools/valgrind/memcheck/suppressions.txt -> memcheck
|
|
tools/valgrind/memcheck/suppressions_mac.txt -> memcheck
|
|
"""
|
|
filename = os.path.abspath(filename)
|
|
parts = filename.split(os.sep)
|
|
tool = parts[-2]
|
|
if tool in ('heapcheck', 'drmemory', 'memcheck', 'tsan'):
|
|
return tool
|
|
return None
|
|
|
|
|
|
def ReadSuppressionsFromFile(filename):
|
|
"""Read suppressions from the given file and return them as a list"""
|
|
tool_to_parser = {
|
|
"drmemory": ReadDrMemorySuppressions,
|
|
"memcheck": ReadValgrindStyleSuppressions,
|
|
"tsan": ReadValgrindStyleSuppressions,
|
|
"heapcheck": ReadValgrindStyleSuppressions,
|
|
}
|
|
tool = FilenameToTool(filename)
|
|
assert tool in tool_to_parser, (
|
|
"unknown tool %s for filename %s" % (tool, filename))
|
|
parse_func = tool_to_parser[tool]
|
|
|
|
# Consider non-existent files to be empty.
|
|
if not os.path.exists(filename):
|
|
return []
|
|
|
|
input_file = file(filename, 'r')
|
|
try:
|
|
return parse_func(input_file, filename)
|
|
except SuppressionError:
|
|
input_file.close()
|
|
raise
|
|
|
|
|
|
class ValgrindStyleSuppression(Suppression):
|
|
"""A suppression using the Valgrind syntax.
|
|
|
|
Most tools, even ones that are not Valgrind-based, use this syntax, ie
|
|
Heapcheck, TSan, etc.
|
|
|
|
Attributes:
|
|
Same as Suppression.
|
|
"""
|
|
|
|
def __init__(self, description, type, stack, defined_at):
|
|
"""Creates a suppression using the Memcheck, TSan, and Heapcheck syntax."""
|
|
regex = '{\n.*\n%s\n' % type
|
|
for line in stack:
|
|
if line == ELLIPSIS:
|
|
regex += '(.*\n)*'
|
|
else:
|
|
regex += GlobToRegex(line)
|
|
regex += '\n'
|
|
regex += '(.*\n)*'
|
|
regex += '}'
|
|
|
|
# In the recent version of valgrind-variant we've switched
|
|
# from memcheck's default Addr[1248]/Value[1248]/Cond suppression types
|
|
# to simply Unaddressable/Uninitialized.
|
|
# The suppression generator no longer gives us "old" types thus
|
|
# for the "new-type" suppressions:
|
|
# * Memcheck:Unaddressable should also match Addr* reports,
|
|
# * Memcheck:Uninitialized should also match Cond and Value reports,
|
|
#
|
|
# We also want to support legacy suppressions (e.g. copied from
|
|
# upstream bugs etc), so:
|
|
# * Memcheck:Addr[1248] suppressions should match Unaddressable reports,
|
|
# * Memcheck:Cond and Memcheck:Value[1248] should match Uninitialized.
|
|
# Please note the latest two rules only apply to the
|
|
# tools/valgrind/waterfall.sh suppression matcher and the real
|
|
# valgrind-variant Memcheck will not suppress
|
|
# e.g. Addr1 printed as Unaddressable with Addr4 suppression.
|
|
# Be careful to check the access size while copying legacy suppressions!
|
|
for sz in [1, 2, 4, 8]:
|
|
regex = regex.replace("\nMemcheck:Addr%d\n" % sz,
|
|
"\nMemcheck:(Addr%d|Unaddressable)\n" % sz)
|
|
regex = regex.replace("\nMemcheck:Value%d\n" % sz,
|
|
"\nMemcheck:(Value%d|Uninitialized)\n" % sz)
|
|
regex = regex.replace("\nMemcheck:Cond\n",
|
|
"\nMemcheck:(Cond|Uninitialized)\n")
|
|
regex = regex.replace("\nMemcheck:Unaddressable\n",
|
|
"\nMemcheck:(Addr.|Unaddressable)\n")
|
|
regex = regex.replace("\nMemcheck:Uninitialized\n",
|
|
"\nMemcheck:(Cond|Value.|Uninitialized)\n")
|
|
|
|
return super(ValgrindStyleSuppression, self).__init__(
|
|
description, type, stack, defined_at, regex)
|
|
|
|
def __str__(self):
|
|
"""Stringify."""
|
|
lines = [self.description, self.type] + self.stack
|
|
return "{\n %s\n}\n" % "\n ".join(lines)
|
|
|
|
|
|
class SuppressionError(Exception):
|
|
def __init__(self, message, happened_at):
|
|
self._message = message
|
|
self._happened_at = happened_at
|
|
|
|
def __str__(self):
|
|
return 'Error reading suppressions at %s!\n%s' % (
|
|
self._happened_at, self._message)
|
|
|
|
|
|
def ReadValgrindStyleSuppressions(lines, supp_descriptor):
|
|
"""Given a list of lines, returns a list of suppressions.
|
|
|
|
Args:
|
|
lines: a list of lines containing suppressions.
|
|
supp_descriptor: should typically be a filename.
|
|
Used only when printing errors.
|
|
"""
|
|
result = []
|
|
cur_descr = ''
|
|
cur_type = ''
|
|
cur_stack = []
|
|
in_suppression = False
|
|
nline = 0
|
|
for line in lines:
|
|
nline += 1
|
|
line = line.strip()
|
|
if line.startswith('#'):
|
|
continue
|
|
if not in_suppression:
|
|
if not line:
|
|
# empty lines between suppressions
|
|
pass
|
|
elif line.startswith('{'):
|
|
in_suppression = True
|
|
pass
|
|
else:
|
|
raise SuppressionError('Expected: "{"',
|
|
"%s:%d" % (supp_descriptor, nline))
|
|
elif line.startswith('}'):
|
|
result.append(
|
|
ValgrindStyleSuppression(cur_descr, cur_type, cur_stack,
|
|
"%s:%d" % (supp_descriptor, nline)))
|
|
cur_descr = ''
|
|
cur_type = ''
|
|
cur_stack = []
|
|
in_suppression = False
|
|
elif not cur_descr:
|
|
cur_descr = line
|
|
continue
|
|
elif not cur_type:
|
|
if (not line.startswith("Memcheck:") and
|
|
not line.startswith("ThreadSanitizer:") and
|
|
(line != "Heapcheck:Leak")):
|
|
raise SuppressionError(
|
|
'Expected "Memcheck:TYPE", "ThreadSanitizer:TYPE" '
|
|
'or "Heapcheck:Leak", got "%s"' % line,
|
|
"%s:%d" % (supp_descriptor, nline))
|
|
supp_type = line.split(':')[1]
|
|
if not supp_type in ["Addr1", "Addr2", "Addr4", "Addr8",
|
|
"Cond", "Free", "Jump", "Leak", "Overlap", "Param",
|
|
"Value1", "Value2", "Value4", "Value8",
|
|
"Race", "UnlockNonLocked", "InvalidLock",
|
|
"Unaddressable", "Uninitialized"]:
|
|
raise SuppressionError('Unknown suppression type "%s"' % supp_type,
|
|
"%s:%d" % (supp_descriptor, nline))
|
|
cur_type = line
|
|
continue
|
|
elif re.match("^fun:.*|^obj:.*|^\.\.\.$", line):
|
|
cur_stack.append(line.strip())
|
|
elif len(cur_stack) == 0 and cur_type == "Memcheck:Param":
|
|
cur_stack.append(line.strip())
|
|
else:
|
|
raise SuppressionError(
|
|
'"fun:function_name" or "obj:object_file" or "..." expected',
|
|
"%s:%d" % (supp_descriptor, nline))
|
|
return result
|
|
|
|
|
|
def PresubmitCheckSuppressions(supps):
|
|
"""Check a list of suppressions and return a list of SuppressionErrors.
|
|
|
|
Mostly useful for separating the checking logic from the Presubmit API for
|
|
testing.
|
|
"""
|
|
known_supp_names = {} # Key: name, Value: suppression.
|
|
errors = []
|
|
for s in supps:
|
|
if re.search("<.*suppression.name.here>", s.description):
|
|
# Suppression name line is
|
|
# <insert_a_suppression_name_here> for Memcheck,
|
|
# <Put your suppression name here> for TSan,
|
|
# name=<insert_a_suppression_name_here> for DrMemory
|
|
errors.append(
|
|
SuppressionError(
|
|
"You've forgotten to put a suppression name like bug_XXX",
|
|
s.defined_at))
|
|
continue
|
|
|
|
if s.description in known_supp_names:
|
|
errors.append(
|
|
SuppressionError(
|
|
'Suppression named "%s" is defined more than once, '
|
|
'see %s' % (s.description,
|
|
known_supp_names[s.description].defined_at),
|
|
s.defined_at))
|
|
else:
|
|
known_supp_names[s.description] = s
|
|
return errors
|
|
|
|
|
|
def PresubmitCheck(input_api, output_api):
|
|
"""A helper function useful in PRESUBMIT.py
|
|
Returns a list of errors or [].
|
|
"""
|
|
sup_regex = re.compile('suppressions.*\.txt$')
|
|
filenames = [f.AbsoluteLocalPath() for f in input_api.AffectedFiles()
|
|
if sup_regex.search(f.LocalPath())]
|
|
|
|
errors = []
|
|
|
|
# TODO(timurrrr): warn on putting suppressions into a wrong file,
|
|
# e.g. TSan suppression in a memcheck file.
|
|
|
|
for f in filenames:
|
|
try:
|
|
supps = ReadSuppressionsFromFile(f)
|
|
errors.extend(PresubmitCheckSuppressions(supps))
|
|
except SuppressionError as e:
|
|
errors.append(e)
|
|
|
|
return [output_api.PresubmitError(str(e)) for e in errors]
|
|
|
|
|
|
class DrMemorySuppression(Suppression):
|
|
"""A suppression using the DrMemory syntax.
|
|
|
|
Attributes:
|
|
instr: The instruction to match.
|
|
Rest inherited from Suppression.
|
|
"""
|
|
|
|
def __init__(self, name, report_type, instr, stack, defined_at):
|
|
"""Constructor."""
|
|
self.instr = instr
|
|
|
|
# Construct the regex.
|
|
regex = '{\n'
|
|
if report_type == 'LEAK':
|
|
regex += '(POSSIBLE )?LEAK'
|
|
else:
|
|
regex += report_type
|
|
regex += '\nname=.*\n'
|
|
|
|
# TODO(rnk): Implement http://crbug.com/107416#c5 .
|
|
# drmemory_analyze.py doesn't generate suppressions with an instruction in
|
|
# them, so these suppressions will always fail to match. We should override
|
|
# Match to fetch the instruction from the report and try to match against
|
|
# that.
|
|
if instr:
|
|
regex += 'instruction=%s\n' % GlobToRegex(instr)
|
|
|
|
for line in stack:
|
|
if line == ELLIPSIS:
|
|
regex += '(.*\n)*'
|
|
elif '!' in line:
|
|
(mod, func) = line.split('!')
|
|
if func == ELLIPSIS: # mod!ellipsis frame
|
|
regex += '(%s\!.*\n)+' % GlobToRegex(mod, ignore_case=True)
|
|
else: # mod!func frame
|
|
# Ignore case for the module match, but not the function match.
|
|
regex += '%s\!%s\n' % (GlobToRegex(mod, ignore_case=True),
|
|
GlobToRegex(func, ignore_case=False))
|
|
else:
|
|
regex += GlobToRegex(line)
|
|
regex += '\n'
|
|
regex += '(.*\n)*' # Match anything left in the stack.
|
|
regex += '}'
|
|
return super(DrMemorySuppression, self).__init__(name, report_type, stack,
|
|
defined_at, regex)
|
|
|
|
def __str__(self):
|
|
"""Stringify."""
|
|
text = self.type + "\n"
|
|
if self.description:
|
|
text += "name=%s\n" % self.description
|
|
if self.instr:
|
|
text += "instruction=%s\n" % self.instr
|
|
text += "\n".join(self.stack)
|
|
text += "\n"
|
|
return text
|
|
|
|
|
|
# Possible DrMemory error report types. Keep consistent with suppress_name
|
|
# array in drmemory/drmemory/report.c.
|
|
DRMEMORY_ERROR_TYPES = [
|
|
'UNADDRESSABLE ACCESS',
|
|
'UNINITIALIZED READ',
|
|
'INVALID HEAP ARGUMENT',
|
|
'GDI USAGE ERROR',
|
|
'HANDLE LEAK',
|
|
'LEAK',
|
|
'POSSIBLE LEAK',
|
|
'WARNING',
|
|
]
|
|
|
|
|
|
# Regexes to match valid drmemory frames.
|
|
DRMEMORY_FRAME_PATTERNS = [
|
|
re.compile(r"^.*\!.*$"), # mod!func
|
|
re.compile(r"^.*!\.\.\.$"), # mod!ellipsis
|
|
re.compile(r"^\<.*\+0x.*\>$"), # <mod+0xoffs>
|
|
re.compile(r"^\<not in a module\>$"),
|
|
re.compile(r"^system call .*$"),
|
|
re.compile(r"^\*$"), # wildcard
|
|
re.compile(r"^\.\.\.$"), # ellipsis
|
|
]
|
|
|
|
|
|
def ReadDrMemorySuppressions(lines, supp_descriptor):
|
|
"""Given a list of lines, returns a list of DrMemory suppressions.
|
|
|
|
Args:
|
|
lines: a list of lines containing suppressions.
|
|
supp_descriptor: should typically be a filename.
|
|
Used only when parsing errors happen.
|
|
"""
|
|
lines = StripAndSkipCommentsIterator(lines)
|
|
suppressions = []
|
|
for (line_no, line) in lines:
|
|
if not line:
|
|
continue
|
|
if line not in DRMEMORY_ERROR_TYPES:
|
|
raise SuppressionError('Expected a DrMemory error type, '
|
|
'found %r instead\n Valid error types: %s' %
|
|
(line, ' '.join(DRMEMORY_ERROR_TYPES)),
|
|
"%s:%d" % (supp_descriptor, line_no))
|
|
|
|
# Suppression starts here.
|
|
report_type = line
|
|
name = ''
|
|
instr = None
|
|
stack = []
|
|
defined_at = "%s:%d" % (supp_descriptor, line_no)
|
|
found_stack = False
|
|
for (line_no, line) in lines:
|
|
if not found_stack and line.startswith('name='):
|
|
name = line.replace('name=', '')
|
|
elif not found_stack and line.startswith('instruction='):
|
|
instr = line.replace('instruction=', '')
|
|
else:
|
|
# Unrecognized prefix indicates start of stack trace.
|
|
found_stack = True
|
|
if not line:
|
|
# Blank line means end of suppression.
|
|
break
|
|
if not any([regex.match(line) for regex in DRMEMORY_FRAME_PATTERNS]):
|
|
raise SuppressionError(
|
|
('Unexpected stack frame pattern at line %d\n' +
|
|
'Frames should be one of the following:\n' +
|
|
' module!function\n' +
|
|
' module!...\n' +
|
|
' <module+0xhexoffset>\n' +
|
|
' <not in a module>\n' +
|
|
' system call Name\n' +
|
|
' *\n' +
|
|
' ...\n') % line_no, defined_at)
|
|
stack.append(line)
|
|
|
|
if len(stack) == 0: # In case we hit EOF or blank without any stack frames.
|
|
raise SuppressionError('Suppression "%s" has no stack frames, ends at %d'
|
|
% (name, line_no), defined_at)
|
|
if stack[-1] == ELLIPSIS:
|
|
raise SuppressionError('Suppression "%s" ends in an ellipsis on line %d' %
|
|
(name, line_no), defined_at)
|
|
|
|
suppressions.append(
|
|
DrMemorySuppression(name, report_type, instr, stack, defined_at))
|
|
|
|
return suppressions
|
|
|
|
|
|
def ParseSuppressionOfType(lines, supp_descriptor, def_line_no, report_type):
|
|
"""Parse the suppression starting on this line.
|
|
|
|
Suppressions start with a type, have an optional name and instruction, and a
|
|
stack trace that ends in a blank line.
|
|
"""
|
|
|
|
|
|
|
|
def TestStack(stack, positive, negative, suppression_parser=None):
|
|
"""A helper function for SelfTest() that checks a single stack.
|
|
|
|
Args:
|
|
stack: the stack to match the suppressions.
|
|
positive: the list of suppressions that must match the given stack.
|
|
negative: the list of suppressions that should not match.
|
|
suppression_parser: optional arg for the suppression parser, default is
|
|
ReadValgrindStyleSuppressions.
|
|
"""
|
|
if not suppression_parser:
|
|
suppression_parser = ReadValgrindStyleSuppressions
|
|
for supp in positive:
|
|
parsed = suppression_parser(supp.split("\n"), "positive_suppression")
|
|
assert parsed[0].Match(stack.split("\n")), (
|
|
"Suppression:\n%s\ndidn't match stack:\n%s" % (supp, stack))
|
|
for supp in negative:
|
|
parsed = suppression_parser(supp.split("\n"), "negative_suppression")
|
|
assert not parsed[0].Match(stack.split("\n")), (
|
|
"Suppression:\n%s\ndid match stack:\n%s" % (supp, stack))
|
|
|
|
|
|
def TestFailPresubmit(supp_text, error_text, suppression_parser=None):
|
|
"""A helper function for SelfTest() that verifies a presubmit check fires.
|
|
|
|
Args:
|
|
supp_text: suppression text to parse.
|
|
error_text: text of the presubmit error we expect to find.
|
|
suppression_parser: optional arg for the suppression parser, default is
|
|
ReadValgrindStyleSuppressions.
|
|
"""
|
|
if not suppression_parser:
|
|
suppression_parser = ReadValgrindStyleSuppressions
|
|
try:
|
|
supps = suppression_parser(supp_text.split("\n"), "<presubmit suppression>")
|
|
except SuppressionError, e:
|
|
# If parsing raised an exception, match the error text here.
|
|
assert error_text in str(e), (
|
|
"presubmit text %r not in SuppressionError:\n%r" %
|
|
(error_text, str(e)))
|
|
else:
|
|
# Otherwise, run the presubmit checks over the supps. We expect a single
|
|
# error that has text matching error_text.
|
|
errors = PresubmitCheckSuppressions(supps)
|
|
assert len(errors) == 1, (
|
|
"expected exactly one presubmit error, got:\n%s" % errors)
|
|
assert error_text in str(errors[0]), (
|
|
"presubmit text %r not in SuppressionError:\n%r" %
|
|
(error_text, str(errors[0])))
|
|
|
|
|
|
def SelfTest():
|
|
"""Tests the Suppression.Match() capabilities."""
|
|
|
|
test_memcheck_stack_1 = """{
|
|
test
|
|
Memcheck:Leak
|
|
fun:absolutly
|
|
fun:brilliant
|
|
obj:condition
|
|
fun:detection
|
|
fun:expression
|
|
}"""
|
|
|
|
test_memcheck_stack_2 = """{
|
|
test
|
|
Memcheck:Uninitialized
|
|
fun:absolutly
|
|
fun:brilliant
|
|
obj:condition
|
|
fun:detection
|
|
fun:expression
|
|
}"""
|
|
|
|
test_memcheck_stack_3 = """{
|
|
test
|
|
Memcheck:Unaddressable
|
|
fun:absolutly
|
|
fun:brilliant
|
|
obj:condition
|
|
fun:detection
|
|
fun:expression
|
|
}"""
|
|
|
|
test_memcheck_stack_4 = """{
|
|
test
|
|
Memcheck:Addr4
|
|
fun:absolutly
|
|
fun:brilliant
|
|
obj:condition
|
|
fun:detection
|
|
fun:expression
|
|
}"""
|
|
|
|
test_heapcheck_stack = """{
|
|
test
|
|
Heapcheck:Leak
|
|
fun:absolutly
|
|
fun:brilliant
|
|
obj:condition
|
|
fun:detection
|
|
fun:expression
|
|
}"""
|
|
|
|
test_tsan_stack = """{
|
|
test
|
|
ThreadSanitizer:Race
|
|
fun:absolutly
|
|
fun:brilliant
|
|
obj:condition
|
|
fun:detection
|
|
fun:expression
|
|
}"""
|
|
|
|
|
|
positive_memcheck_suppressions_1 = [
|
|
"{\nzzz\nMemcheck:Leak\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Leak\nfun:ab*ly\n}",
|
|
"{\nzzz\nMemcheck:Leak\nfun:absolutly\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Leak\n...\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Leak\n...\nfun:detection\n}",
|
|
"{\nzzz\nMemcheck:Leak\nfun:absolutly\n...\nfun:detection\n}",
|
|
"{\nzzz\nMemcheck:Leak\nfun:ab*ly\n...\nfun:detection\n}",
|
|
"{\nzzz\nMemcheck:Leak\n...\nobj:condition\n}",
|
|
"{\nzzz\nMemcheck:Leak\n...\nobj:condition\nfun:detection\n}",
|
|
"{\nzzz\nMemcheck:Leak\n...\nfun:brilliant\nobj:condition\n}",
|
|
]
|
|
|
|
positive_memcheck_suppressions_2 = [
|
|
"{\nzzz\nMemcheck:Uninitialized\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Uninitialized\nfun:ab*ly\n}",
|
|
"{\nzzz\nMemcheck:Uninitialized\nfun:absolutly\nfun:brilliant\n}",
|
|
# Legacy suppression types
|
|
"{\nzzz\nMemcheck:Value1\n...\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Cond\n...\nfun:detection\n}",
|
|
"{\nzzz\nMemcheck:Value8\nfun:absolutly\nfun:brilliant\n}",
|
|
]
|
|
|
|
positive_memcheck_suppressions_3 = [
|
|
"{\nzzz\nMemcheck:Unaddressable\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Unaddressable\nfun:absolutly\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Unaddressable\nfun:absolutly\nfun:brilliant\n}",
|
|
# Legacy suppression types
|
|
"{\nzzz\nMemcheck:Addr1\n...\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Addr8\n...\nfun:detection\n}",
|
|
]
|
|
|
|
positive_memcheck_suppressions_4 = [
|
|
"{\nzzz\nMemcheck:Addr4\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Unaddressable\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Addr4\nfun:absolutly\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Unaddressable\n...\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Addr4\n...\nfun:detection\n}",
|
|
]
|
|
|
|
positive_heapcheck_suppressions = [
|
|
"{\nzzz\nHeapcheck:Leak\n...\nobj:condition\n}",
|
|
"{\nzzz\nHeapcheck:Leak\nfun:absolutly\n}",
|
|
]
|
|
|
|
positive_tsan_suppressions = [
|
|
"{\nzzz\nThreadSanitizer:Race\n...\nobj:condition\n}",
|
|
"{\nzzz\nThreadSanitizer:Race\nfun:absolutly\n}",
|
|
]
|
|
|
|
negative_memcheck_suppressions_1 = [
|
|
"{\nzzz\nMemcheck:Leak\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Leak\nfun:ab*liant\n}",
|
|
"{\nzzz\nMemcheck:Leak\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Leak\nobj:condition\n}",
|
|
"{\nzzz\nMemcheck:Addr8\nfun:brilliant\n}",
|
|
]
|
|
|
|
negative_memcheck_suppressions_2 = [
|
|
"{\nzzz\nMemcheck:Cond\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Value2\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Uninitialized\nfun:ab*liant\n}",
|
|
"{\nzzz\nMemcheck:Value4\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Leak\nobj:condition\n}",
|
|
"{\nzzz\nMemcheck:Addr8\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Unaddressable\nfun:brilliant\n}",
|
|
]
|
|
|
|
negative_memcheck_suppressions_3 = [
|
|
"{\nzzz\nMemcheck:Addr1\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Uninitialized\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Addr2\nfun:ab*liant\n}",
|
|
"{\nzzz\nMemcheck:Value4\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Leak\nobj:condition\n}",
|
|
"{\nzzz\nMemcheck:Addr8\nfun:brilliant\n}",
|
|
]
|
|
|
|
negative_memcheck_suppressions_4 = [
|
|
"{\nzzz\nMemcheck:Addr1\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Addr4\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Unaddressable\nfun:abnormal\n}",
|
|
"{\nzzz\nMemcheck:Addr1\nfun:absolutly\n}",
|
|
"{\nzzz\nMemcheck:Addr2\nfun:ab*liant\n}",
|
|
"{\nzzz\nMemcheck:Value4\nfun:brilliant\n}",
|
|
"{\nzzz\nMemcheck:Leak\nobj:condition\n}",
|
|
"{\nzzz\nMemcheck:Addr8\nfun:brilliant\n}",
|
|
]
|
|
|
|
negative_heapcheck_suppressions = [
|
|
"{\nzzz\nMemcheck:Leak\nfun:absolutly\n}",
|
|
"{\nzzz\nHeapcheck:Leak\nfun:brilliant\n}",
|
|
]
|
|
|
|
negative_tsan_suppressions = [
|
|
"{\nzzz\nThreadSanitizer:Leak\nfun:absolutly\n}",
|
|
"{\nzzz\nThreadSanitizer:Race\nfun:brilliant\n}",
|
|
]
|
|
|
|
TestStack(test_memcheck_stack_1,
|
|
positive_memcheck_suppressions_1,
|
|
negative_memcheck_suppressions_1)
|
|
TestStack(test_memcheck_stack_2,
|
|
positive_memcheck_suppressions_2,
|
|
negative_memcheck_suppressions_2)
|
|
TestStack(test_memcheck_stack_3,
|
|
positive_memcheck_suppressions_3,
|
|
negative_memcheck_suppressions_3)
|
|
TestStack(test_memcheck_stack_4,
|
|
positive_memcheck_suppressions_4,
|
|
negative_memcheck_suppressions_4)
|
|
TestStack(test_heapcheck_stack, positive_heapcheck_suppressions,
|
|
negative_heapcheck_suppressions)
|
|
TestStack(test_tsan_stack, positive_tsan_suppressions,
|
|
negative_tsan_suppressions)
|
|
|
|
# TODO(timurrrr): add TestFailPresubmit tests.
|
|
|
|
### DrMemory self tests.
|
|
|
|
# http://crbug.com/96010 suppression.
|
|
stack_96010 = """{
|
|
UNADDRESSABLE ACCESS
|
|
name=<insert_a_suppression_name_here>
|
|
*!TestingProfile::FinishInit
|
|
*!TestingProfile::TestingProfile
|
|
*!BrowserAboutHandlerTest_WillHandleBrowserAboutURL_Test::TestBody
|
|
*!testing::Test::Run
|
|
}"""
|
|
|
|
suppress_96010 = [
|
|
"UNADDRESSABLE ACCESS\nname=zzz\n...\n*!testing::Test::Run\n",
|
|
("UNADDRESSABLE ACCESS\nname=zzz\n...\n" +
|
|
"*!BrowserAboutHandlerTest_WillHandleBrowserAboutURL_Test::TestBody\n"),
|
|
"UNADDRESSABLE ACCESS\nname=zzz\n...\n*!BrowserAboutHandlerTest*\n",
|
|
"UNADDRESSABLE ACCESS\nname=zzz\n*!TestingProfile::FinishInit\n",
|
|
# No name should be needed
|
|
"UNADDRESSABLE ACCESS\n*!TestingProfile::FinishInit\n",
|
|
# Whole trace
|
|
("UNADDRESSABLE ACCESS\n" +
|
|
"*!TestingProfile::FinishInit\n" +
|
|
"*!TestingProfile::TestingProfile\n" +
|
|
"*!BrowserAboutHandlerTest_WillHandleBrowserAboutURL_Test::TestBody\n" +
|
|
"*!testing::Test::Run\n"),
|
|
]
|
|
|
|
negative_96010 = [
|
|
# Wrong type
|
|
"UNINITIALIZED READ\nname=zzz\n*!TestingProfile::FinishInit\n",
|
|
# No ellipsis
|
|
"UNADDRESSABLE ACCESS\nname=zzz\n*!BrowserAboutHandlerTest*\n",
|
|
]
|
|
|
|
TestStack(stack_96010, suppress_96010, negative_96010,
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Invalid heap arg
|
|
stack_invalid = """{
|
|
INVALID HEAP ARGUMENT
|
|
name=asdf
|
|
*!foo
|
|
}"""
|
|
suppress_invalid = [
|
|
"INVALID HEAP ARGUMENT\n*!foo\n",
|
|
]
|
|
negative_invalid = [
|
|
"UNADDRESSABLE ACCESS\n*!foo\n",
|
|
]
|
|
|
|
TestStack(stack_invalid, suppress_invalid, negative_invalid,
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Suppress only ntdll
|
|
stack_in_ntdll = """{
|
|
UNADDRESSABLE ACCESS
|
|
name=<insert_a_suppression_name_here>
|
|
ntdll.dll!RtlTryEnterCriticalSection
|
|
}"""
|
|
stack_not_ntdll = """{
|
|
UNADDRESSABLE ACCESS
|
|
name=<insert_a_suppression_name_here>
|
|
notntdll.dll!RtlTryEnterCriticalSection
|
|
}"""
|
|
|
|
suppress_in_ntdll = [
|
|
"UNADDRESSABLE ACCESS\nntdll.dll!RtlTryEnterCriticalSection\n",
|
|
]
|
|
suppress_in_any = [
|
|
"UNADDRESSABLE ACCESS\n*!RtlTryEnterCriticalSection\n",
|
|
]
|
|
|
|
TestStack(stack_in_ntdll, suppress_in_ntdll + suppress_in_any, [],
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
# Make sure we don't wildcard away the "not" part and match ntdll.dll by
|
|
# accident.
|
|
TestStack(stack_not_ntdll, suppress_in_any, suppress_in_ntdll,
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Suppress a POSSIBLE LEAK with LEAK.
|
|
stack_foo_possible = """{
|
|
POSSIBLE LEAK
|
|
name=foo possible
|
|
*!foo
|
|
}"""
|
|
suppress_foo_possible = [ "POSSIBLE LEAK\n*!foo\n" ]
|
|
suppress_foo_leak = [ "LEAK\n*!foo\n" ]
|
|
TestStack(stack_foo_possible, suppress_foo_possible + suppress_foo_leak, [],
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Don't suppress LEAK with POSSIBLE LEAK.
|
|
stack_foo_leak = """{
|
|
LEAK
|
|
name=foo leak
|
|
*!foo
|
|
}"""
|
|
TestStack(stack_foo_leak, suppress_foo_leak, suppress_foo_possible,
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Test case insensitivity of module names.
|
|
stack_user32_mixed_case = """{
|
|
LEAK
|
|
name=<insert>
|
|
USER32.dll!foo
|
|
user32.DLL!bar
|
|
user32.dll!baz
|
|
}"""
|
|
suppress_user32 = [ # Module name case doesn't matter.
|
|
"LEAK\nuser32.dll!foo\nuser32.dll!bar\nuser32.dll!baz\n",
|
|
"LEAK\nUSER32.DLL!foo\nUSER32.DLL!bar\nUSER32.DLL!baz\n",
|
|
]
|
|
no_suppress_user32 = [ # Function name case matters.
|
|
"LEAK\nuser32.dll!FOO\nuser32.dll!BAR\nuser32.dll!BAZ\n",
|
|
"LEAK\nUSER32.DLL!FOO\nUSER32.DLL!BAR\nUSER32.DLL!BAZ\n",
|
|
]
|
|
TestStack(stack_user32_mixed_case, suppress_user32, no_suppress_user32,
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Test mod!... frames.
|
|
stack_kernel32_through_ntdll = """{
|
|
LEAK
|
|
name=<insert>
|
|
kernel32.dll!foo
|
|
KERNEL32.dll!bar
|
|
kernel32.DLL!baz
|
|
ntdll.dll!quux
|
|
}"""
|
|
suppress_mod_ellipsis = [
|
|
"LEAK\nkernel32.dll!...\nntdll.dll!quux\n",
|
|
"LEAK\nKERNEL32.DLL!...\nntdll.dll!quux\n",
|
|
]
|
|
no_suppress_mod_ellipsis = [
|
|
# Need one or more matching frames, not zero, unlike regular ellipsis.
|
|
"LEAK\nuser32.dll!...\nkernel32.dll!...\nntdll.dll!quux\n",
|
|
]
|
|
TestStack(stack_kernel32_through_ntdll, suppress_mod_ellipsis,
|
|
no_suppress_mod_ellipsis,
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Test that the presubmit checks work.
|
|
forgot_to_name = """
|
|
UNADDRESSABLE ACCESS
|
|
name=<insert_a_suppression_name_here>
|
|
ntdll.dll!RtlTryEnterCriticalSection
|
|
"""
|
|
TestFailPresubmit(forgot_to_name, 'forgotten to put a suppression',
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
named_twice = """
|
|
UNADDRESSABLE ACCESS
|
|
name=http://crbug.com/1234
|
|
*!foo
|
|
|
|
UNADDRESSABLE ACCESS
|
|
name=http://crbug.com/1234
|
|
*!bar
|
|
"""
|
|
TestFailPresubmit(named_twice, 'defined more than once',
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
forgot_stack = """
|
|
UNADDRESSABLE ACCESS
|
|
name=http://crbug.com/1234
|
|
"""
|
|
TestFailPresubmit(forgot_stack, 'has no stack frames',
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
ends_in_ellipsis = """
|
|
UNADDRESSABLE ACCESS
|
|
name=http://crbug.com/1234
|
|
ntdll.dll!RtlTryEnterCriticalSection
|
|
...
|
|
"""
|
|
TestFailPresubmit(ends_in_ellipsis, 'ends in an ellipsis',
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
bad_stack_frame = """
|
|
UNADDRESSABLE ACCESS
|
|
name=http://crbug.com/1234
|
|
fun:memcheck_style_frame
|
|
"""
|
|
TestFailPresubmit(bad_stack_frame, 'Unexpected stack frame pattern',
|
|
suppression_parser=ReadDrMemorySuppressions)
|
|
|
|
# Test FilenameToTool.
|
|
filenames_to_tools = {
|
|
"tools/heapcheck/suppressions.txt": "heapcheck",
|
|
"tools/valgrind/tsan/suppressions.txt": "tsan",
|
|
"tools/valgrind/drmemory/suppressions.txt": "drmemory",
|
|
"tools/valgrind/drmemory/suppressions_full.txt": "drmemory",
|
|
"tools/valgrind/memcheck/suppressions.txt": "memcheck",
|
|
"tools/valgrind/memcheck/suppressions_mac.txt": "memcheck",
|
|
"asdf/tools/valgrind/memcheck/suppressions_mac.txt": "memcheck",
|
|
"foo/bar/baz/tools/valgrind/memcheck/suppressions_mac.txt": "memcheck",
|
|
"foo/bar/baz/tools/valgrind/suppressions.txt": None,
|
|
"tools/valgrind/suppressions.txt": None,
|
|
}
|
|
for (filename, expected_tool) in filenames_to_tools.items():
|
|
filename.replace('/', os.sep) # Make the path look native.
|
|
tool = FilenameToTool(filename)
|
|
assert tool == expected_tool, (
|
|
"failed to get expected tool for filename %r, expected %s, got %s" %
|
|
(filename, expected_tool, tool))
|
|
|
|
# Test ValgrindStyleSuppression.__str__.
|
|
supp = ValgrindStyleSuppression("http://crbug.com/1234", "Memcheck:Leak",
|
|
["...", "fun:foo"], "supp.txt:1")
|
|
# Intentional 3-space indent. =/
|
|
supp_str = ("{\n"
|
|
" http://crbug.com/1234\n"
|
|
" Memcheck:Leak\n"
|
|
" ...\n"
|
|
" fun:foo\n"
|
|
"}\n")
|
|
assert str(supp) == supp_str, (
|
|
"str(supp) != supp_str:\nleft: %s\nright: %s" % (str(supp), supp_str))
|
|
|
|
# Test DrMemorySuppression.__str__.
|
|
supp = DrMemorySuppression(
|
|
"http://crbug.com/1234", "LEAK", None, ["...", "*!foo"], "supp.txt:1")
|
|
supp_str = ("LEAK\n"
|
|
"name=http://crbug.com/1234\n"
|
|
"...\n"
|
|
"*!foo\n")
|
|
assert str(supp) == supp_str, (
|
|
"str(supp) != supp_str:\nleft: %s\nright: %s" % (str(supp), supp_str))
|
|
|
|
supp = DrMemorySuppression(
|
|
"http://crbug.com/1234", "UNINITIALIZED READ", "test 0x08(%eax) $0x01",
|
|
["ntdll.dll!*", "*!foo"], "supp.txt:1")
|
|
supp_str = ("UNINITIALIZED READ\n"
|
|
"name=http://crbug.com/1234\n"
|
|
"instruction=test 0x08(%eax) $0x01\n"
|
|
"ntdll.dll!*\n"
|
|
"*!foo\n")
|
|
assert str(supp) == supp_str, (
|
|
"str(supp) != supp_str:\nleft: %s\nright: %s" % (str(supp), supp_str))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
SelfTest()
|
|
print 'PASS'
|