diff options
author | dkegel@google.com <dkegel@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-16 15:55:52 +0000 |
---|---|---|
committer | dkegel@google.com <dkegel@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-16 15:55:52 +0000 |
commit | 68e3b47230aa88a848ba6cb7daee852993f51a62 (patch) | |
tree | 15203472cb668e0a6277c99a1ed1c5b0d67fec2c /tools | |
parent | 9229b2de068f352b3d184996206870d1d4ffc2b5 (diff) | |
download | chromium_src-68e3b47230aa88a848ba6cb7daee852993f51a62.zip chromium_src-68e3b47230aa88a848ba6cb7daee852993f51a62.tar.gz chromium_src-68e3b47230aa88a848ba6cb7daee852993f51a62.tar.bz2 |
Patch from timurrrr: Re-factor valgrind scripts to add tsan support
First reviewed at http://codereview.chromium.org/125272
BUG=none
TEST=none
Review URL: http://codereview.chromium.org/155528
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@20870 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools')
-rwxr-xr-x | tools/valgrind/chrome_tests.py | 60 | ||||
-rw-r--r-- | tools/valgrind/memcheck/suppressions.txt (renamed from tools/valgrind/suppressions.txt) | 0 | ||||
-rw-r--r-- | tools/valgrind/memcheck/suppressions_mac.txt (renamed from tools/valgrind/suppressions_mac.txt) | 0 | ||||
-rwxr-xr-x | tools/valgrind/memcheck_analyze.py (renamed from tools/valgrind/valgrind_analyze.py) | 14 | ||||
-rw-r--r-- | tools/valgrind/tsan/ignores.txt | 39 | ||||
-rw-r--r-- | tools/valgrind/tsan/suppressions.txt | 1 | ||||
-rw-r--r-- | tools/valgrind/tsan_analyze.py | 85 | ||||
-rwxr-xr-x | tools/valgrind/valgrind.sh | 59 | ||||
-rwxr-xr-x | tools/valgrind/valgrind_test.py | 512 |
9 files changed, 521 insertions, 249 deletions
diff --git a/tools/valgrind/chrome_tests.py b/tools/valgrind/chrome_tests.py index c7f9e8a..0a65793 100755 --- a/tools/valgrind/chrome_tests.py +++ b/tools/valgrind/chrome_tests.py @@ -31,7 +31,7 @@ import google.path_utils import layout_package.platform_utils import common - +import valgrind_test class TestNotFound(Exception): pass @@ -107,8 +107,8 @@ class ChromeTests: # since this path is used for string matching, make sure it's always # an absolute Windows-style path self._source_dir = utility.GetAbsolutePath(self._source_dir) - valgrind_test = os.path.join(script_dir, "valgrind_test.py") - self._command_preamble = ["python", valgrind_test, + valgrind_test_script = os.path.join(script_dir, "valgrind_test.py") + self._command_preamble = [valgrind_test_script, "--source_dir=%s" % (self._source_dir)] def _DefaultCommand(self, module, exe=None, valgrind_test_args=None): @@ -140,7 +140,9 @@ class ChromeTests: cmd = list(self._command_preamble) for directory in self._data_dirs: - suppression_file = os.path.join(directory, "suppressions.txt") + tool_name = self._options.valgrind_tool + suppression_file = os.path.join(directory, + "%s/suppressions.txt" % tool_name) if os.path.exists(suppression_file): cmd.append("--suppressions=%s" % suppression_file) # Platform specific suppression @@ -149,25 +151,14 @@ class ChromeTests: 'linux2': 'linux' }[sys.platform] suppression_file_platform = \ - os.path.join(directory, 'suppressions_%s.txt' % suppression_platform) + os.path.join(directory, + '%s/suppressions_%s.txt' % (tool_name, suppression_platform)) if os.path.exists(suppression_file_platform): cmd.append("--suppressions=%s" % suppression_file_platform) - if self._options.baseline: - cmd.append("--baseline") - if self._options.verbose: - cmd.append("--verbose") - if self._options.show_all_leaks: - cmd.append("--show_all_leaks") - if self._options.track_origins: - cmd.append("--track_origins") - if self._options.generate_dsym: - cmd.append("--generate_dsym") - if self._options.generate_suppressions: - cmd.append("--generate_suppressions") - if self._options.custom_valgrind_command: - cmd.append("--custom_valgrind_command=%s" - % self._options.custom_valgrind_command) + cmd.append("--tool=%s" % self._options.valgrind_tool) + if self._options.valgrind_tool_flags: + cmd += self._options.valgrind_tool_flags.split(" ") if valgrind_test_args != None: for arg in valgrind_test_args: cmd.append(arg) @@ -193,7 +184,11 @@ class ChromeTests: 'linux2': 'linux'}[sys.platform] gtest_filter_files = [ os.path.join(directory, name + ".gtest.txt"), - os.path.join(directory, name + ".gtest_%s.txt" % platform_suffix)] + os.path.join(directory, name + ".gtest-%s.txt" % \ + self._options.valgrind_tool), + os.path.join(directory, name + ".gtest_%s.txt" % platform_suffix), + os.path.join(directory, name + ".gtest-%s_%s.txt" % \ + (self._options.valgrind_tool, platform_suffix))] for filename in gtest_filter_files: if os.path.exists(filename): logging.info("reading gtest filters from %s" % filename) @@ -221,7 +216,7 @@ class ChromeTests: if cmd_args: cmd.extend(["--"]) cmd.extend(cmd_args) - return common.RunSubprocess(cmd, 0) + return valgrind_test.RunTool(cmd) def TestBase(self): return self.SimpleTest("base", "base_unittests") @@ -324,8 +319,7 @@ class ChromeTests: # Now run script_cmd with the wrapper in cmd cmd.extend(["--"]) cmd.extend(script_cmd) - ret = common.RunSubprocess(cmd, 0) - return ret + return valgrind_test.RunTool(cmd) def TestLayout(self): # A "chunk file" is maintained in the local directory so that each test @@ -387,20 +381,10 @@ def _main(_): help="additional arguments to --gtest_filter") parser.add_option("-v", "--verbose", action="store_true", default=False, help="verbose output - enable debug log messages") - parser.add_option("", "--show_all_leaks", action="store_true", - default=False, - help="also show even less blatant leaks") - parser.add_option("", "--track_origins", action="store_true", - default=False, - help="Show whence uninit bytes came. 30% slower.") - parser.add_option("", "--generate_dsym", action="store_true", - default=False, - help="Generate .dSYM file on Mac if needed. Slow!") - parser.add_option("", "--generate_suppressions", action="store_true", - default=False, - help="Skip analysis and generate suppressions") - parser.add_option("", "--custom_valgrind_command", - help="Use custom valgrind command and options") + parser.add_option("", "--tool", dest="valgrind_tool", default="memcheck", + help="specify a valgrind tool to run the tests under") + parser.add_option("", "--tool_flags", dest="valgrind_tool_flags", default="", + help="specify custom flags for the selected valgrind tool") # My machine can do about 120 layout tests/hour in release mode. # Let's do 30 minutes worth per run. # The CPU is mostly idle, so perhaps we can raise this when diff --git a/tools/valgrind/suppressions.txt b/tools/valgrind/memcheck/suppressions.txt index 9cac256..9cac256 100644 --- a/tools/valgrind/suppressions.txt +++ b/tools/valgrind/memcheck/suppressions.txt diff --git a/tools/valgrind/suppressions_mac.txt b/tools/valgrind/memcheck/suppressions_mac.txt index 5eb9261..5eb9261 100644 --- a/tools/valgrind/suppressions_mac.txt +++ b/tools/valgrind/memcheck/suppressions_mac.txt diff --git a/tools/valgrind/valgrind_analyze.py b/tools/valgrind/memcheck_analyze.py index 9fa0f12..a108d36 100755 --- a/tools/valgrind/valgrind_analyze.py +++ b/tools/valgrind/memcheck_analyze.py @@ -3,7 +3,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -# valgrind_analyze.py +# memcheck_analyze.py ''' Given a valgrind XML file, parses errors and uniques them.''' @@ -176,7 +176,7 @@ class ValgrindError: def __eq__(self, rhs): return self.UniqueString() == rhs -class ValgrindAnalyze: +class MemcheckAnalyze: ''' Given a set of Valgrind XML files, parse all the errors out of them, unique them and output the results.''' @@ -192,6 +192,7 @@ class ValgrindAnalyze: self._errors = set() badfiles = set() start = time.time() + self._parse_failed = False for file in files: # Wait up to three minutes for valgrind to finish writing all files, # but after that, just skip incomplete files and warn. @@ -217,6 +218,7 @@ class ValgrindAnalyze: getTextOf(raw_error, "kind") != "Leak_PossiblyLost"): self._errors.add(ValgrindError(source_dir, raw_error)) except ExpatError, e: + self._parse_failed = True logging.warn("could not parse %s: %s" % (file, e)) lineno = e.lineno - 1 context_lines = 5 @@ -237,6 +239,10 @@ class ValgrindAnalyze: logging.warn("valgrind didn't finish writing %d files?!" % len(badfiles)) def Report(self): + if self._parse_failed: + logging.error("FAIL! Couldn't parse Valgrind output file") + return -2 + if self._errors: logging.error("FAIL! There were %s errors: " % len(self._errors)) @@ -249,7 +255,7 @@ class ValgrindAnalyze: return 0 def _main(): - '''For testing only. The ValgrindAnalyze class should be imported instead.''' + '''For testing only. The MemcheckAnalyze class should be imported instead.''' retcode = 0 parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") parser.add_option("", "--source_dir", @@ -261,7 +267,7 @@ def _main(): parser.error("no filename specified") filenames = args - analyzer = ValgrindAnalyze(options.source_dir, filenames) + analyzer = MemcheckAnalyze(options.source_dir, filenames) retcode = analyzer.Report() sys.exit(retcode) diff --git a/tools/valgrind/tsan/ignores.txt b/tools/valgrind/tsan/ignores.txt new file mode 100644 index 0000000..e1f0972 --- /dev/null +++ b/tools/valgrind/tsan/ignores.txt @@ -0,0 +1,39 @@ +# This file lists the functions, object files and source files +# which should be ignored (i.e. not instrumented) by ThreadSanitizer. + +obj:*/ld-2* +obj:*/libpthread-* + +fun:clone +fun:fork +fun:pthread_* +fun:__lll_mutex_unlock_wake +fun:__cxa_atexit +fun:__new_exitfn + +src:*ts_valgrind_intercepts.c + +# Chromium-specific +fun:*_ZN4base6subtle* + + +# dark magic with 'errno' here. +#fun:sys_ptrace_detach +#fun:sys_ptrace +#fun:sys_stat +fun:sys_* + +# ignore libc's printf functions +fun:_IO_* +fun:vfprintf +fun:fwrite +fun:fflush + + +# 32-bit version of std::string +# There is something very strange inside this function. +# Looks like they write zero to a global variable which +# value is already zero. I.e. the race is benign. +# Global var is _ZNSs4_Rep20_S_empty_rep_storageE +fun:*_M_mutateEjjj + diff --git a/tools/valgrind/tsan/suppressions.txt b/tools/valgrind/tsan/suppressions.txt new file mode 100644 index 0000000..c0e0008 --- /dev/null +++ b/tools/valgrind/tsan/suppressions.txt @@ -0,0 +1 @@ +# There are no suppressions here yet. diff --git a/tools/valgrind/tsan_analyze.py b/tools/valgrind/tsan_analyze.py new file mode 100644 index 0000000..13f3bc7 --- /dev/null +++ b/tools/valgrind/tsan_analyze.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# Copyright (c) 2006-2008 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. + +# tsan_analyze.py + +''' Given a ThreadSanitizer output file, parses errors and uniques them.''' + +import logging +import optparse +import os +import re +import subprocess +import sys +import time + +class TsanAnalyze: + ''' Given a set of ThreadSanitizer output files, parse all the errors out of + them, unique them and output the results.''' + + def __init__(self, source_dir, files): + '''Reads in a set of files. + + Args: + source_dir: Path to top of source tree for this build + files: A list of filenames. + ''' + + self.races = [] + for file in files: + self.races = self.races + self.GetReportFrom(file) + + def GetReportFrom(self, filename): + ret = [] + f = open(filename, 'r') + while True: + line = f.readline() + if (line == ''): + break + tmp = "" + while (re.match(".*INFO: T.* " + + "(has been created by T.* at this point|is program's main thread)" + ".*", line)): + tmp = tmp + line + while not re.match(".*}}}.*", line): + line = f.readline() + tmp = tmp + line + line = f.readline() + + if (re.match(".*Possible data race.*", line)): + tmp = tmp + line + while not re.match(".*}}}.*", line): + line = f.readline() + tmp = tmp + line + ret.append(tmp.strip()) + f.close() + return ret + + def Report(self): + if len(self.races) > 0: + logging.error("Found %i race reports" % len(self.races)) + for report in self.races: + logging.error("\n" + report) + return -1 + logging.info("PASS: No race reports found") + return 0 + +if __name__ == '__main__': + '''For testing only. The TsanAnalyze class should be imported instead.''' + retcode = 0 + parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") + parser.add_option("", "--source_dir", + help="path to top of source tree for this build" + "(used to normalize source paths in baseline)") + + (options, args) = parser.parse_args() + if not len(args) >= 1: + parser.error("no filename specified") + filenames = args + + analyzer = TsanAnalyze(options.source_dir, filenames) + retcode = analyzer.Report() + + sys.exit(retcode) diff --git a/tools/valgrind/valgrind.sh b/tools/valgrind/valgrind.sh index 0170038..94e571e 100755 --- a/tools/valgrind/valgrind.sh +++ b/tools/valgrind/valgrind.sh @@ -12,6 +12,31 @@ # To run unit tests, you probably want to run chrome_tests.sh instead. # That's the script used by the valgrind buildbot. +setup_memcheck() { + # Prefer a 32-bit gdb if it's available. + GDB="/usr/bin/gdb32"; + if [ ! -x $GDB ]; then + GDB="gdb" + fi + + # Prompt to attach gdb when there was an error detected. + DEFAULT_TOOL_FLAGS=("--db-command=$GDB -nw %f %p" "--db-attach=yes" \ + # Overwrite newly allocated or freed objects + # with 0x41 to catch inproper use. + "--malloc-fill=41" "--free-fill=41") +} + +setup_tsan() { + IGNORE_FILE="$(cd `dirname "$0"` && pwd)/tsan/ignores.txt" + DEFAULT_TOOL_FLAGS=("--announce-threads" "--pure-happens-before=yes" \ + "--ignore=$IGNORE_FILE") +} + +setup_unknown() { + echo "Unknown tool \"$TOOL_NAME\" specified, the result is not guaranteed" + DEFAULT_TOOL_FLAGS=() +} + set -e if [ $# -eq 0 ]; then @@ -19,21 +44,36 @@ if [ $# -eq 0 ]; then exit 1 fi -# Prefer a 32-bit gdb if it's available. -GDB="/usr/bin/gdb32"; -if [ ! -x $GDB ]; then - GDB="gdb" +TOOL_NAME="memcheck" +declare -a DEFAULT_TOOL_FLAGS[0] + +# Select a tool different from memcheck with --tool=TOOL as a first argument +TMP_STR=`echo $1 | sed 's/^\-\-tool=//'` +if [ "$TMP_STR" != "$1" ]; then + TOOL_NAME="$TMP_STR" + shift +fi + +if echo "$@" | grep "\-\-tool" ; then + echo "--tool=TOOL must be the first argument" >&2 + exit 1 fi +case $TOOL_NAME in + memcheck*) setup_memcheck;; + tsan*) setup_tsan;; + *) setup_unknown;; +esac + # Prefer a local install of valgrind if it's available. VALGRIND="/usr/local/bin/valgrind" if [ ! -x $VALGRIND ]; then VALGRIND="valgrind" fi -SUPPRESSIONS="$(cd `dirname "$0"` && pwd)/suppressions.txt" +SUPPRESSIONS="$(cd `dirname "$0"` && pwd)/$TOOL_NAME/suppressions.txt" -set -v +set -x # Pass GTK glib allocations through to system malloc so valgrind sees them. # Prevent NSS from recycling memory arenas so valgrind can track origins. @@ -41,15 +81,14 @@ set -v # Overwrite newly allocated or freed objects with 0x41 to catch inproper use. # smc-check=all is required for valgrind to see v8's dynamic code generation. # trace-children to follow into the renderer processes. -# Prompt to attach gdb when there was an error detected. + G_SLICE=always-malloc \ NSS_DISABLE_ARENA_FREE_LIST=1 \ G_DEBUG=fatal_warnings \ "$VALGRIND" \ + --tool=$TOOL_NAME \ --trace-children=yes \ - --db-command="$GDB -nw %f %p" \ - --db-attach=yes \ --suppressions="$SUPPRESSIONS" \ - --malloc-fill=41 --free-fill=41 \ --smc-check=all \ + "${DEFAULT_TOOL_FLAGS[@]}" \ "$@" diff --git a/tools/valgrind/valgrind_test.py b/tools/valgrind/valgrind_test.py index 66332ea..c9e7e46 100755 --- a/tools/valgrind/valgrind_test.py +++ b/tools/valgrind/valgrind_test.py @@ -3,7 +3,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -# purify_test.py +# valgrind_test.py '''Runs an exe through Valgrind and puts the intermediate files in a directory. @@ -22,13 +22,12 @@ import tempfile import common -import valgrind_analyze +import memcheck_analyze +import tsan_analyze import google.logging_utils -rmtree = shutil.rmtree - -class Valgrind(object): +class ValgrindTool(object): """Abstract class for running Valgrind. @@ -44,6 +43,10 @@ class Valgrind(object): shutil.rmtree(self.TMP_DIR) os.mkdir(self.TMP_DIR) + def ToolName(self): + raise RuntimeError, "This method should be implemented " \ + "in the tool-specific subclass" + def CreateOptionParser(self): self._parser = optparse.OptionParser("usage: %prog [options] <program to " "test>") @@ -53,67 +56,187 @@ class Valgrind(object): self._parser.add_option("", "--source_dir", help="path to top of source tree for this build" "(used to normalize source paths in baseline)") - self._parser.add_option("", "--suppressions", default=[], - action="append", - help="path to a valgrind suppression file") self._parser.add_option("", "--gtest_filter", default="", help="which test case to run") self._parser.add_option("", "--gtest_print_time", action="store_true", default=False, help="show how long each test takes") - self._parser.add_option("", "--trace_children", action="store_true", - default=False, - help="also trace child processes") self._parser.add_option("", "--indirect", action="store_true", default=False, - help="set BROWSER_WRAPPER rather than running valgrind directly") - self._parser.add_option("", "--show_all_leaks", action="store_true", - default=False, - help="also show less blatant leaks") - self._parser.add_option("", "--track_origins", action="store_true", + help="set BROWSER_WRAPPER rather than " + "running valgrind directly") + self._parser.add_option("-v", "--verbose", action="store_true", default=False, - help="Show whence uninit bytes came. 30% slower.") - self._parser.add_option("", "--generate_dsym", action="store_true", + help="verbose output - enable debug log messages") + self._parser.add_option("", "--trace_children", action="store_true", default=False, - help="Generate .dSYM file on Mac if needed. Slow!") - self._parser.add_option("", "--generate_suppressions", action="store_true", + help="also trace child processes") + self._parser.add_option("", "--gen_suppressions", action="store_true", + dest="generate_suppressions", default=False, + help="skip analysis and generate suppressions") + self._parser.add_option("", "--num-callers", + dest="num_callers", default=30, + help="number of callers to show in stack traces") + self._parser.add_option("", "--nocleanup_on_exit", action="store_true", default=False, - help="Skip analysis and generate suppressions") - self._parser.add_option("", "--custom_valgrind_command", - help="Use custom valgrind command and options") - self._parser.add_option("-v", "--verbose", action="store_true", default=False, - help="verbose output - enable debug log messages") + help="don't delete directory with logs on exit") + self.ExtendOptionParser(self._parser) self._parser.description = __doc__ - def ParseArgv(self): + def ExtendOptionParser(self, parser): + if sys.platform == 'darwin': + parser.add_option("", "--generate_dsym", action="store_true", + default=False, + help="Generate .dSYM file on Mac if needed. Slow!") + + def ParseArgv(self, args): self.CreateOptionParser() - self._options, self._args = self._parser.parse_args() + + # self._tool_flags will store those tool flags which we don't parse + # manually in this script. + self._tool_flags = [] + known_args = [] + + """ We assume that the first argument not starting with "-" is a program name + and all the following flags should be passed to the program. + TODO(timurrrr): customize optparse instead + """ + while len(args) > 0 and args[0][:1] == "-": + arg = args[0] + if (arg == "--"): + break + if self._parser.has_option(arg.split("=")[0]): + known_args += [arg] + else: + self._tool_flags += [arg] + args = args[1:] + + if len(args) > 0: + known_args += args + + self._options, self._args = self._parser.parse_args(known_args) + self._timeout = int(self._options.timeout) - self._suppressions = self._options.suppressions + self._num_callers = int(self._options.num_callers) self._generate_suppressions = self._options.generate_suppressions + self._suppressions = self._options.suppressions self._source_dir = self._options.source_dir + self._nocleanup_on_exit = self._options.nocleanup_on_exit if self._options.gtest_filter != "": self._args.append("--gtest_filter=%s" % self._options.gtest_filter) if self._options.gtest_print_time: self._args.append("--gtest_print_time"); - if self._options.verbose: - google.logging_utils.config_root(logging.DEBUG) - else: - google.logging_utils.config_root() return True - def Setup(self): - return self.ParseArgv() + def Setup(self, args): + return self.ParseArgv(args) def PrepareForTest(self): - """Perform necessary tasks prior to executing the test.""" - pass + """Runs dsymutil if needed. + + Valgrind for Mac OS X requires that debugging information be in a .dSYM + bundle generated by dsymutil. It is not currently able to chase DWARF + data into .o files like gdb does, so executables without .dSYM bundles or + with the Chromium-specific "fake_dsym" bundles generated by + build/mac/strip_save_dsym won't give source file and line number + information in valgrind. + + This function will run dsymutil if the .dSYM bundle is missing or if + it looks like a fake_dsym. A non-fake dsym that already exists is assumed + to be up-to-date. + """ + if sys.platform != 'darwin': + return + + test_command = self._args[0] + dsym_bundle = self._args[0] + '.dSYM' + dsym_file = os.path.join(dsym_bundle, 'Contents', 'Resources', 'DWARF', + os.path.basename(test_command)) + dsym_info_plist = os.path.join(dsym_bundle, 'Contents', 'Info.plist') + + needs_dsymutil = True + saved_test_command = None + + if os.path.exists(dsym_file) and os.path.exists(dsym_info_plist): + # Look for the special fake_dsym tag in dsym_info_plist. + dsym_info_plist_contents = open(dsym_info_plist).read() + + if not re.search('^\s*<key>fake_dsym</key>$', dsym_info_plist_contents, + re.MULTILINE): + # fake_dsym is not set, this is a real .dSYM bundle produced by + # dsymutil. dsymutil does not need to be run again. + needs_dsymutil = False + else: + # fake_dsym is set. dsym_file is a copy of the original test_command + # before it was stripped. Copy it back to test_command so that + # dsymutil has unstripped input to work with. Move the stripped + # test_command out of the way, it will be restored when this is + # done. + saved_test_command = test_command + '.stripped' + os.rename(test_command, saved_test_command) + shutil.copyfile(dsym_file, test_command) + shutil.copymode(saved_test_command, test_command) + + if needs_dsymutil: + if self._options.generate_dsym: + # Remove the .dSYM bundle if it exists. + shutil.rmtree(dsym_bundle, True) + + dsymutil_command = ['dsymutil', test_command] + + # dsymutil is crazy slow. Let it run for up to a half hour. I hope + # that's enough. + common.RunSubprocess(dsymutil_command, 30 * 60) + + if saved_test_command: + os.rename(saved_test_command, test_command) + else: + logging.info("No real .dSYM for test_command. Line numbers will " + "not be shown. Either tell xcode to generate .dSYM " + "file, or use --generate_dsym option to this tool.") def ValgrindCommand(self): """Get the valgrind command to run.""" - raise RuntimeError, "Never use Valgrind directly. Always subclass and " \ - "implement ValgrindCommand() at least" + # Note that self._args begins with the exe to be run. + tool_name = self.ToolName() + + # Construct the valgrind command. + proc = ["valgrind", + "--tool=%s" % tool_name, + "--smc-check=all", + "--num-callers=%i" % self._num_callers] + + if self._options.trace_children: + proc += ["--trace-children=yes"]; + + proc += self.ToolSpecificFlags() + proc += self._tool_flags + + if self._generate_suppressions: + proc += ["--gen-suppressions=all"] + + suppression_count = 0 + for suppression_file in self._suppressions: + if os.path.exists(suppression_file): + suppression_count += 1 + proc += ["--suppressions=%s" % suppression_file] + + if not suppression_count: + logging.warning("WARNING: NOT USING SUPPRESSIONS!") + + proc += ["--log-file=" + self.TMP_DIR + ("/%s." % tool_name) + "%p"] + + # The Valgrind command is constructed. + + if self._options.indirect: + self.CreateBrowserWrapper(" ".join(proc)) + proc = [] + proc += self._args + return proc + + def ToolSpecificFlags(self): + return [] def Execute(self): ''' Execute the app to be tested after successful instrumentation. @@ -134,28 +257,14 @@ class Valgrind(object): return True def Analyze(self): - # Glob all the files in the "valgrind.tmp" directory - filenames = glob.glob(self.TMP_DIR + "/valgrind.*") - # TODO(dkegel): use new xml suppressions feature when it lands - if self._generate_suppressions: - # Just concatenate all the output files. Lame... - for filename in filenames: - print "## %s" % filename - f = file(filename) - while True: - line = f.readline() - if len(line) == 0: - break - print line, # comma means don't add newline - f.close() - return 0 - analyzer = valgrind_analyze.ValgrindAnalyze(self._source_dir, filenames, self._options.show_all_leaks) - return analyzer.Report() + raise RuntimeError, "This method should be implemented " \ + "in the tool-specific subclass" def Cleanup(self): # Right now, we can cleanup by deleting our temporary directory. Other # cleanup is still a TODO? - shutil.rmtree(self.TMP_DIR) + if not self._nocleanup_on_exit: + shutil.rmtree(self.TMP_DIR, ignore_errors=True) return True def RunTestsAndAnalyze(self): @@ -169,11 +278,28 @@ class Valgrind(object): logging.info("Execution and analysis completed successfully.") return 0 - def Main(self): + def CreateBrowserWrapper(self, command): + """The program being run invokes Python or something else + that can't stand to be valgrinded, and also invokes + the Chrome browser. Set an environment variable to + tell the program to prefix the Chrome commandline + with a magic wrapper. Build the magic wrapper here. + """ + (fd, indirect_fname) = tempfile.mkstemp(dir=self.TMP_DIR, prefix="browser_wrapper.", text=True) + f = os.fdopen(fd, "w"); + f.write("#!/bin/sh\n") + f.write(command) + f.write(' "$@"\n') + f.close() + os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR) + os.putenv("BROWSER_WRAPPER", indirect_fname) + logging.info('export BROWSER_WRAPPER=' + indirect_fname); + + def Main(self, args): '''Call this to run through the whole process: Setup, Execute, Analyze''' start = datetime.datetime.now() retcode = -1 - if self.Setup(): + if self.Setup(args): retcode = self.RunTestsAndAnalyze() self.Cleanup() else: @@ -187,156 +313,148 @@ class Valgrind(object): logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds)) return retcode - -class ValgrindLinux(Valgrind): - - """Valgrind on Linux.""" +# TODO(timurrrr): Split into a separate file. +class Memcheck(ValgrindTool): + """Memcheck""" def __init__(self): - Valgrind.__init__(self) - - def ValgrindCommand(self): - """Get the valgrind command to run.""" - # note that self._args begins with the exe to be run - - if self._options.custom_valgrind_command: - # take the full valgrind command from --custom_valgrind_command - proc = self._options.custom_valgrind_command.split() - else: - # construct the valgrind command - proc = ["valgrind", "--smc-check=all", "--leak-check=full", - "--num-callers=50"] - - if self._options.show_all_leaks: - proc += ["--show-reachable=yes"]; - - if self._options.track_origins: - proc += ["--track-origins=yes"]; - - if self._options.trace_children: - proc += ["--trace-children=yes"]; - - # Either generate suppressions or load them. - # TODO(dkegel): enhance valgrind to support generating - # suppressions in xml mode. See - # http://bugs.kde.org/show_bug.cgi?id=191189 - if self._generate_suppressions: - proc += ["--gen-suppressions=all"] - else: - proc += ["--xml=yes"] - - suppression_count = 0 - for suppression_file in self._suppressions: - if os.path.exists(suppression_file): - suppression_count += 1 - proc += ["--suppressions=%s" % suppression_file] - - if not suppression_count: - logging.warning("WARNING: NOT USING SUPPRESSIONS!") - - proc += ["--log-file=" + self.TMP_DIR + "/valgrind.%p"] - - # The Valgrind command is constructed. - - if self._options.indirect: - # The program being run invokes Python or something else - # that can't stand to be valgrinded, and also invokes - # the Chrome browser. Set an environment variable to - # tell the program to prefix the Chrome commandline - # with a magic wrapper. Build the magic wrapper here. - (fd, indirect_fname) = tempfile.mkstemp(dir=self.TMP_DIR, prefix="browser_wrapper.", text=True) - f = os.fdopen(fd, "w"); - f.write("#!/bin/sh\n") - f.write(" ".join(proc)) - f.write(' "$@"\n') - f.close() - os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR) - os.putenv("BROWSER_WRAPPER", indirect_fname) - logging.info('export BROWSER_WRAPPER=' + indirect_fname); - proc = [] - - proc += self._args - return proc - - -class ValgrindMac(ValgrindLinux): - - """Valgrind on Mac OS X. - Same as Linux, but currently needs one extra step to run dsymutil. - This will go away once we update our valgrind. - """ - - def PrepareForTest(self): - """Runs dsymutil if needed. - - Valgrind for Mac OS X requires that debugging information be in a .dSYM - bundle generated by dsymutil. It is not currently able to chase DWARF - data into .o files like gdb does, so executables without .dSYM bundles or - with the Chromium-specific "fake_dsym" bundles generated by - build/mac/strip_save_dsym won't give source file and line number - information in valgrind. - - This function will run dsymutil if the .dSYM bundle is missing or if - it looks like a fake_dsym. A non-fake dsym that already exists is assumed - to be up-to-date. + ValgrindTool.__init__(self) + + def ToolName(self): + return "memcheck" + + def ExtendOptionParser(self, parser): + ValgrindTool.ExtendOptionParser(self, parser) + parser.add_option("", "--suppressions", default=[], + action="append", + help="path to a valgrind suppression file") + parser.add_option("", "--show_all_leaks", action="store_true", + default=False, + help="also show less blatant leaks") + parser.add_option("", "--track_origins", action="store_true", + default=False, + help="Show whence uninitialized bytes came. 30% slower.") + + def ToolSpecificFlags(self): + ret = ["--leak-check=full"] + + if self._options.show_all_leaks: + ret += ["--show-reachable=yes"]; + + if self._options.track_origins: + ret += ["--track-origins=yes"]; + + """Either generate suppressions or load them. + TODO(dkegel): enhance valgrind to support generating + suppressions in xml mode. See + http://bugs.kde.org/show_bug.cgi?id=191189 """ + if self._generate_suppressions: + ret += ["--gen-suppressions=all"] + else: + ret += ["--xml=yes"] - test_command = self._args[0] - dsym_bundle = self._args[0] + '.dSYM' - dsym_file = os.path.join(dsym_bundle, 'Contents', 'Resources', 'DWARF', - os.path.basename(test_command)) - dsym_info_plist = os.path.join(dsym_bundle, 'Contents', 'Info.plist') + return ret - needs_dsymutil = True - saved_test_command = None + def Analyze(self): + # Glob all the files in the "valgrind.tmp" directory + filenames = glob.glob(self.TMP_DIR + "/memcheck.*") - if os.path.exists(dsym_file) and os.path.exists(dsym_info_plist): - # Look for the special fake_dsym tag in dsym_info_plist. - dsym_info_plist_contents = open(dsym_info_plist).read() + # TODO(dkegel): use new xml suppressions feature when it lands + if self._generate_suppressions: + # Just concatenate all the output files. Lame... + for filename in filenames: + print "## %s" % filename + f = file(filename) + while True: + line = f.readline() + if len(line) == 0: + break + print line, # comma means don't add newline + f.close() + return 0 + analyzer = memcheck_analyze.MemcheckAnalyze(self._source_dir, filenames, self._options.show_all_leaks) + return analyzer.Report() - if not re.search('^\s*<key>fake_dsym</key>$', dsym_info_plist_contents, - re.MULTILINE): - # fake_dsym is not set, this is a real .dSYM bundle produced by - # dsymutil. dsymutil does not need to be run again. - needs_dsymutil = False - else: - # fake_dsym is set. dsym_file is a copy of the original test_command - # before it was stripped. Copy it back to test_command so that - # dsymutil has unstripped input to work with. Move the stripped - # test_command out of the way, it will be restored when this is - # done. - saved_test_command = test_command + '.stripped' - os.rename(test_command, saved_test_command) - shutil.copyfile(dsym_file, test_command) - shutil.copymode(saved_test_command, test_command) +class ThreadSanitizer(ValgrindTool): + """ThreadSanitizer""" - if needs_dsymutil: - if self._options.generate_dsym: - # Remove the .dSYM bundle if it exists. - shutil.rmtree(dsym_bundle, True) + def __init__(self): + ValgrindTool.__init__(self) + + def ToolName(self): + return "tsan" + + def ExtendOptionParser(self, parser): + ValgrindTool.ExtendOptionParser(self, parser) + parser.add_option("", "--suppressions", default=[], + action="append", + help="path to a valgrind suppression file") + parser.add_option("", "--pure-happens-before", default="yes", + dest="pure_happens_before", + help="Less false reports, more missed races") + parser.add_option("", "--announce-threads", default="yes", + dest="announce_threads", + help="Show the the stack traces of thread creation") + + def EvalBoolFlag(self, flag_value): + if (flag_value in ["1", "true", "yes"]): + return True + elif (flag_value in ["0", "false", "no"]): + return False + raise RuntimeError, "Can't parse flag value (%s)" % flag_value + + def ToolSpecificFlags(self): + ret = ["--ignore=%s" % \ + os.path.join(self._source_dir, + "tools", "valgrind", "tsan", "ignores.txt")] + ret += ["--file-prefix-to-cut=%s/" % self._source_dir] + + if self.EvalBoolFlag(self._options.pure_happens_before): + ret += ["--pure-happens-before=yes"]; + + if self.EvalBoolFlag(self._options.announce_threads): + ret += ["--announce-threads"] + + return ret - dsymutil_command = ['dsymutil', test_command] + def Analyze(self): + filenames = glob.glob(self.TMP_DIR + "/tsan.*") + analyzer = tsan_analyze.TsanAnalyze(self._source_dir, filenames) + return analyzer.Report() - # dsymutil is crazy slow. Let it run for up to a half hour. I hope - # that's enough. - common.RunSubprocess(dsymutil_command, 30 * 60) - if saved_test_command: - os.rename(saved_test_command, test_command) - else: - logging.info("No real .dSYM for test_command. Line numbers will " - "not be shown. Either tell xcode to generate .dSYM " - "file, or use --generate_dsym option to this tool.") +class ToolFactory: + def Create(self, tool_name): + if tool_name == "memcheck": + return Memcheck() + if tool_name == "tsan": + if sys.platform != 'linux2': + logging.info("WARNING: ThreadSanitizer is not working yet on Mac") + return ThreadSanitizer() + raise RuntimeError, "Unknown tool" \ + "(tool=%s, platform=%s)" % \ + (tool_name, sys.platform) + +def RunTool(argv): + # TODO(timurrrr): customize optparse instead + tool_name = "memcheck" + args = argv[1:] + for arg in args: + if arg.startswith("--tool="): + tool_name = arg[7:] + args.remove(arg) + break + + tool = ToolFactory().Create(tool_name) + return tool.Main(args) if __name__ == "__main__": - if sys.platform == 'darwin': # Mac - valgrind = ValgrindMac() - retcode = valgrind.Main() - sys.exit(retcode) - elif sys.platform == 'linux2': # Linux - valgrind = ValgrindLinux() - retcode = valgrind.Main() - sys.exit(retcode) + if sys.argv.count("-v") > 0 or sys.argv.count("--verbose") > 0: + google.logging_utils.config_root(logging.DEBUG) else: - logging.error("Unknown platform: %s" % sys.platform) - sys.exit(1) + google.logging_utils.config_root() + # TODO(timurrrr): valgrind tools may use -v/--verbose as well + + ret = RunTool(sys.argv) + sys.exit(ret) |