Fix some CSS presubmit issues.
[chromium-blink-merge.git] / tools / valgrind / valgrind_test.py
blob24fa32acb49850935a7ccbdd846b9fb08de821a7
1 # Copyright (c) 2012 The Chromium 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.
5 """Runs an exe through Valgrind and puts the intermediate files in a
6 directory.
7 """
9 import datetime
10 import glob
11 import logging
12 import optparse
13 import os
14 import re
15 import shutil
16 import stat
17 import subprocess
18 import sys
19 import tempfile
21 import common
23 import drmemory_analyze
24 import memcheck_analyze
25 import tsan_analyze
27 class BaseTool(object):
28 """Abstract class for running Valgrind-, PIN-based and other dynamic
29 error detector tools.
31 Always subclass this and implement ToolCommand with framework- and
32 tool-specific stuff.
33 """
35 def __init__(self):
36 temp_parent_dir = None
37 self.log_parent_dir = ""
38 if common.IsWindows():
39 # gpu process on Windows Vista+ runs at Low Integrity and can only
40 # write to certain directories (http://crbug.com/119131)
42 # TODO(bruening): if scripts die in middle and don't clean up temp
43 # dir, we'll accumulate files in profile dir. should remove
44 # really old files automatically.
45 profile = os.getenv("USERPROFILE")
46 if profile:
47 self.log_parent_dir = profile + "\\AppData\\LocalLow\\"
48 if os.path.exists(self.log_parent_dir):
49 self.log_parent_dir = common.NormalizeWindowsPath(self.log_parent_dir)
50 temp_parent_dir = self.log_parent_dir
51 # Generated every time (even when overridden)
52 self.temp_dir = tempfile.mkdtemp(prefix="vg_logs_", dir=temp_parent_dir)
53 self.log_dir = self.temp_dir # overridable by --keep_logs
54 self.option_parser_hooks = []
55 # TODO(glider): we may not need some of the env vars on some of the
56 # platforms.
57 self._env = {
58 "G_SLICE" : "always-malloc",
59 "NSS_DISABLE_UNLOAD" : "1",
60 "NSS_DISABLE_ARENA_FREE_LIST" : "1",
61 "GTEST_DEATH_TEST_USE_FORK": "1",
64 def ToolName(self):
65 raise NotImplementedError, "This method should be implemented " \
66 "in the tool-specific subclass"
68 def Analyze(self, check_sanity=False):
69 raise NotImplementedError, "This method should be implemented " \
70 "in the tool-specific subclass"
72 def RegisterOptionParserHook(self, hook):
73 # Frameworks and tools can add their own flags to the parser.
74 self.option_parser_hooks.append(hook)
76 def CreateOptionParser(self):
77 # Defines Chromium-specific flags.
78 self._parser = optparse.OptionParser("usage: %prog [options] <program to "
79 "test>")
80 self._parser.disable_interspersed_args()
81 self._parser.add_option("-t", "--timeout",
82 dest="timeout", metavar="TIMEOUT", default=10000,
83 help="timeout in seconds for the run (default 10000)")
84 self._parser.add_option("", "--build-dir",
85 help="the location of the compiler output")
86 self._parser.add_option("", "--source-dir",
87 help="path to top of source tree for this build"
88 "(used to normalize source paths in baseline)")
89 self._parser.add_option("", "--gtest_filter", default="",
90 help="which test case to run")
91 self._parser.add_option("", "--gtest_repeat",
92 help="how many times to run each test")
93 self._parser.add_option("", "--gtest_print_time", action="store_true",
94 default=False,
95 help="show how long each test takes")
96 self._parser.add_option("", "--ignore_exit_code", action="store_true",
97 default=False,
98 help="ignore exit code of the test "
99 "(e.g. test failures)")
100 self._parser.add_option("", "--keep_logs", action="store_true",
101 default=False,
102 help="store memory tool logs in the <tool>.logs "
103 "directory instead of /tmp.\nThis can be "
104 "useful for tool developers/maintainers.\n"
105 "Please note that the <tool>.logs directory "
106 "will be clobbered on tool startup.")
108 # To add framework- or tool-specific flags, please add a hook using
109 # RegisterOptionParserHook in the corresponding subclass.
110 # See ValgrindTool and ThreadSanitizerBase for examples.
111 for hook in self.option_parser_hooks:
112 hook(self, self._parser)
114 def ParseArgv(self, args):
115 self.CreateOptionParser()
117 # self._tool_flags will store those tool flags which we don't parse
118 # manually in this script.
119 self._tool_flags = []
120 known_args = []
122 """ We assume that the first argument not starting with "-" is a program
123 name and all the following flags should be passed to the program.
124 TODO(timurrrr): customize optparse instead
126 while len(args) > 0 and args[0][:1] == "-":
127 arg = args[0]
128 if (arg == "--"):
129 break
130 if self._parser.has_option(arg.split("=")[0]):
131 known_args += [arg]
132 else:
133 self._tool_flags += [arg]
134 args = args[1:]
136 if len(args) > 0:
137 known_args += args
139 self._options, self._args = self._parser.parse_args(known_args)
141 self._timeout = int(self._options.timeout)
142 self._source_dir = self._options.source_dir
143 if self._options.keep_logs:
144 # log_parent_dir has trailing slash if non-empty
145 self.log_dir = self.log_parent_dir + "%s.logs" % self.ToolName()
146 if os.path.exists(self.log_dir):
147 shutil.rmtree(self.log_dir)
148 os.mkdir(self.log_dir)
149 logging.info("Logs are in " + self.log_dir)
151 self._ignore_exit_code = self._options.ignore_exit_code
152 if self._options.gtest_filter != "":
153 self._args.append("--gtest_filter=%s" % self._options.gtest_filter)
154 if self._options.gtest_repeat:
155 self._args.append("--gtest_repeat=%s" % self._options.gtest_repeat)
156 if self._options.gtest_print_time:
157 self._args.append("--gtest_print_time")
159 return True
161 def Setup(self, args):
162 return self.ParseArgv(args)
164 def ToolCommand(self):
165 raise NotImplementedError, "This method should be implemented " \
166 "in the tool-specific subclass"
168 def Cleanup(self):
169 # You may override it in the tool-specific subclass
170 pass
172 def Execute(self):
173 """ Execute the app to be tested after successful instrumentation.
174 Full execution command-line provided by subclassers via proc."""
175 logging.info("starting execution...")
176 proc = self.ToolCommand()
177 for var in self._env:
178 common.PutEnvAndLog(var, self._env[var])
179 return common.RunSubprocess(proc, self._timeout)
181 def RunTestsAndAnalyze(self, check_sanity):
182 exec_retcode = self.Execute()
183 analyze_retcode = self.Analyze(check_sanity)
185 if analyze_retcode:
186 logging.error("Analyze failed.")
187 logging.info("Search the log for '[ERROR]' to see the error reports.")
188 return analyze_retcode
190 if exec_retcode:
191 if self._ignore_exit_code:
192 logging.info("Test execution failed, but the exit code is ignored.")
193 else:
194 logging.error("Test execution failed.")
195 return exec_retcode
196 else:
197 logging.info("Test execution completed successfully.")
199 if not analyze_retcode:
200 logging.info("Analysis completed successfully.")
202 return 0
204 def Main(self, args, check_sanity, min_runtime_in_seconds):
205 """Call this to run through the whole process: Setup, Execute, Analyze"""
206 start_time = datetime.datetime.now()
207 retcode = -1
208 if self.Setup(args):
209 retcode = self.RunTestsAndAnalyze(check_sanity)
210 shutil.rmtree(self.temp_dir, ignore_errors=True)
211 self.Cleanup()
212 else:
213 logging.error("Setup failed")
214 end_time = datetime.datetime.now()
215 runtime_in_seconds = (end_time - start_time).seconds
216 hours = runtime_in_seconds / 3600
217 seconds = runtime_in_seconds % 3600
218 minutes = seconds / 60
219 seconds = seconds % 60
220 logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds))
221 if (min_runtime_in_seconds > 0 and
222 runtime_in_seconds < min_runtime_in_seconds):
223 logging.error("Layout tests finished too quickly. "
224 "It should have taken at least %d seconds. "
225 "Something went wrong?" % min_runtime_in_seconds)
226 retcode = -1
227 return retcode
229 def Run(self, args, module, min_runtime_in_seconds=0):
230 MODULES_TO_SANITY_CHECK = ["base"]
232 # TODO(timurrrr): this is a temporary workaround for http://crbug.com/47844
233 if self.ToolName() == "tsan" and common.IsMac():
234 MODULES_TO_SANITY_CHECK = []
236 check_sanity = module in MODULES_TO_SANITY_CHECK
237 return self.Main(args, check_sanity, min_runtime_in_seconds)
240 class ValgrindTool(BaseTool):
241 """Abstract class for running Valgrind tools.
243 Always subclass this and implement ToolSpecificFlags() and
244 ExtendOptionParser() for tool-specific stuff.
246 def __init__(self):
247 super(ValgrindTool, self).__init__()
248 self.RegisterOptionParserHook(ValgrindTool.ExtendOptionParser)
250 def UseXML(self):
251 # Override if tool prefers nonxml output
252 return True
254 def SelfContained(self):
255 # Returns true iff the tool is distibuted as a self-contained
256 # .sh script (e.g. ThreadSanitizer)
257 return False
259 def ExtendOptionParser(self, parser):
260 parser.add_option("", "--suppressions", default=[],
261 action="append",
262 help="path to a valgrind suppression file")
263 parser.add_option("", "--indirect", action="store_true",
264 default=False,
265 help="set BROWSER_WRAPPER rather than "
266 "running valgrind directly")
267 parser.add_option("", "--indirect_webkit_layout", action="store_true",
268 default=False,
269 help="set --wrapper rather than running Dr. Memory "
270 "directly.")
271 parser.add_option("", "--trace_children", action="store_true",
272 default=False,
273 help="also trace child processes")
274 parser.add_option("", "--num-callers",
275 dest="num_callers", default=30,
276 help="number of callers to show in stack traces")
277 parser.add_option("", "--generate_dsym", action="store_true",
278 default=False,
279 help="Generate .dSYM file on Mac if needed. Slow!")
281 def Setup(self, args):
282 if not BaseTool.Setup(self, args):
283 return False
284 if common.IsMac():
285 self.PrepareForTestMac()
286 return True
288 def PrepareForTestMac(self):
289 """Runs dsymutil if needed.
291 Valgrind for Mac OS X requires that debugging information be in a .dSYM
292 bundle generated by dsymutil. It is not currently able to chase DWARF
293 data into .o files like gdb does, so executables without .dSYM bundles or
294 with the Chromium-specific "fake_dsym" bundles generated by
295 build/mac/strip_save_dsym won't give source file and line number
296 information in valgrind.
298 This function will run dsymutil if the .dSYM bundle is missing or if
299 it looks like a fake_dsym. A non-fake dsym that already exists is assumed
300 to be up-to-date.
302 test_command = self._args[0]
303 dsym_bundle = self._args[0] + '.dSYM'
304 dsym_file = os.path.join(dsym_bundle, 'Contents', 'Resources', 'DWARF',
305 os.path.basename(test_command))
306 dsym_info_plist = os.path.join(dsym_bundle, 'Contents', 'Info.plist')
308 needs_dsymutil = True
309 saved_test_command = None
311 if os.path.exists(dsym_file) and os.path.exists(dsym_info_plist):
312 # Look for the special fake_dsym tag in dsym_info_plist.
313 dsym_info_plist_contents = open(dsym_info_plist).read()
315 if not re.search('^\s*<key>fake_dsym</key>$', dsym_info_plist_contents,
316 re.MULTILINE):
317 # fake_dsym is not set, this is a real .dSYM bundle produced by
318 # dsymutil. dsymutil does not need to be run again.
319 needs_dsymutil = False
320 else:
321 # fake_dsym is set. dsym_file is a copy of the original test_command
322 # before it was stripped. Copy it back to test_command so that
323 # dsymutil has unstripped input to work with. Move the stripped
324 # test_command out of the way, it will be restored when this is
325 # done.
326 saved_test_command = test_command + '.stripped'
327 os.rename(test_command, saved_test_command)
328 shutil.copyfile(dsym_file, test_command)
329 shutil.copymode(saved_test_command, test_command)
331 if needs_dsymutil:
332 if self._options.generate_dsym:
333 # Remove the .dSYM bundle if it exists.
334 shutil.rmtree(dsym_bundle, True)
336 dsymutil_command = ['dsymutil', test_command]
338 # dsymutil is crazy slow. Ideally we'd have a timeout here,
339 # but common.RunSubprocess' timeout is only checked
340 # after each line of output; dsymutil is silent
341 # until the end, and is then killed, which is silly.
342 common.RunSubprocess(dsymutil_command)
344 if saved_test_command:
345 os.rename(saved_test_command, test_command)
346 else:
347 logging.info("No real .dSYM for test_command. Line numbers will "
348 "not be shown. Either tell xcode to generate .dSYM "
349 "file, or use --generate_dsym option to this tool.")
351 def ToolCommand(self):
352 """Get the valgrind command to run."""
353 # Note that self._args begins with the exe to be run.
354 tool_name = self.ToolName()
356 # Construct the valgrind command.
357 if self.SelfContained():
358 proc = ["valgrind-%s.sh" % tool_name]
359 else:
360 if 'CHROME_VALGRIND' in os.environ:
361 path = os.path.join(os.environ['CHROME_VALGRIND'], "bin", "valgrind")
362 else:
363 path = "valgrind"
364 proc = [path, "--tool=%s" % tool_name]
366 proc += ["--num-callers=%i" % int(self._options.num_callers)]
368 if self._options.trace_children:
369 proc += ["--trace-children=yes"]
370 proc += ["--trace-children-skip='*dbus-daemon*'"]
371 proc += ["--trace-children-skip='*dbus-launch*'"]
372 proc += ["--trace-children-skip='*perl*'"]
373 proc += ["--trace-children-skip='*python*'"]
374 # This is really Python, but for some reason Valgrind follows it.
375 proc += ["--trace-children-skip='*lsb_release*'"]
377 proc += self.ToolSpecificFlags()
378 proc += self._tool_flags
380 suppression_count = 0
381 for suppression_file in self._options.suppressions:
382 if os.path.exists(suppression_file):
383 suppression_count += 1
384 proc += ["--suppressions=%s" % suppression_file]
386 if not suppression_count:
387 logging.warning("WARNING: NOT USING SUPPRESSIONS!")
389 logfilename = self.log_dir + ("/%s." % tool_name) + "%p"
390 if self.UseXML():
391 proc += ["--xml=yes", "--xml-file=" + logfilename]
392 else:
393 proc += ["--log-file=" + logfilename]
395 # The Valgrind command is constructed.
397 # Valgrind doesn't play nice with the Chrome sandbox. Empty this env var
398 # set by runtest.py to disable the sandbox.
399 if os.environ.get("CHROME_DEVEL_SANDBOX", None):
400 logging.info("Removing CHROME_DEVEL_SANDBOX from environment")
401 os.environ["CHROME_DEVEL_SANDBOX"] = ''
403 # Handle --indirect_webkit_layout separately.
404 if self._options.indirect_webkit_layout:
405 # Need to create the wrapper before modifying |proc|.
406 wrapper = self.CreateBrowserWrapper(proc, webkit=True)
407 proc = self._args
408 proc.append("--wrapper")
409 proc.append(wrapper)
410 return proc
412 if self._options.indirect:
413 wrapper = self.CreateBrowserWrapper(proc)
414 os.environ["BROWSER_WRAPPER"] = wrapper
415 logging.info('export BROWSER_WRAPPER=' + wrapper)
416 proc = []
417 proc += self._args
418 return proc
420 def ToolSpecificFlags(self):
421 raise NotImplementedError, "This method should be implemented " \
422 "in the tool-specific subclass"
424 def CreateBrowserWrapper(self, proc, webkit=False):
425 """The program being run invokes Python or something else that can't stand
426 to be valgrinded, and also invokes the Chrome browser. In this case, use a
427 magic wrapper to only valgrind the Chrome browser. Build the wrapper here.
428 Returns the path to the wrapper. It's up to the caller to use the wrapper
429 appropriately.
431 command = " ".join(proc)
432 # Add the PID of the browser wrapper to the logfile names so we can
433 # separate log files for different UI tests at the analyze stage.
434 command = command.replace("%p", "$$.%p")
436 (fd, indirect_fname) = tempfile.mkstemp(dir=self.log_dir,
437 prefix="browser_wrapper.",
438 text=True)
439 f = os.fdopen(fd, "w")
440 f.write('#!/bin/bash\n'
441 'echo "Started Valgrind wrapper for this test, PID=$$" >&2\n')
443 f.write('DIR=`dirname $0`\n'
444 'TESTNAME_FILE=$DIR/testcase.$$.name\n\n')
446 if webkit:
447 # Webkit layout_tests pass the URL as the first line of stdin.
448 f.write('tee $TESTNAME_FILE | %s "$@"\n' % command)
449 else:
450 # Try to get the test case name by looking at the program arguments.
451 # i.e. Chromium ui_tests used --test-name arg.
452 # TODO(timurrrr): This doesn't handle "--test-name Test.Name"
453 # TODO(timurrrr): ui_tests are dead. Where do we use the non-webkit
454 # wrapper now? browser_tests? What do they do?
455 f.write('for arg in $@\ndo\n'
456 ' if [[ "$arg" =~ --test-name=(.*) ]]\n then\n'
457 ' echo ${BASH_REMATCH[1]} >$TESTNAME_FILE\n'
458 ' fi\n'
459 'done\n\n'
460 '%s "$@"\n' % command)
462 f.close()
463 os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR)
464 return indirect_fname
466 def CreateAnalyzer(self):
467 raise NotImplementedError, "This method should be implemented " \
468 "in the tool-specific subclass"
470 def GetAnalyzeResults(self, check_sanity=False):
471 # Glob all the files in the log directory
472 filenames = glob.glob(self.log_dir + "/" + self.ToolName() + ".*")
474 # If we have browser wrapper, the logfiles are named as
475 # "toolname.wrapper_PID.valgrind_PID".
476 # Let's extract the list of wrapper_PIDs and name it ppids
477 ppids = set([int(f.split(".")[-2]) \
478 for f in filenames if re.search("\.[0-9]+\.[0-9]+$", f)])
480 analyzer = self.CreateAnalyzer()
481 if len(ppids) == 0:
482 # Fast path - no browser wrapper was set.
483 return analyzer.Report(filenames, None, check_sanity)
485 ret = 0
486 for ppid in ppids:
487 testcase_name = None
488 try:
489 f = open(self.log_dir + ("/testcase.%d.name" % ppid))
490 testcase_name = f.read().strip()
491 f.close()
492 wk_layout_prefix="third_party/WebKit/LayoutTests/"
493 wk_prefix_at = testcase_name.rfind(wk_layout_prefix)
494 if wk_prefix_at != -1:
495 testcase_name = testcase_name[wk_prefix_at + len(wk_layout_prefix):]
496 except IOError:
497 pass
498 print "====================================================="
499 print " Below is the report for valgrind wrapper PID=%d." % ppid
500 if testcase_name:
501 print " It was used while running the `%s` test." % testcase_name
502 else:
503 print " You can find the corresponding test"
504 print " by searching the above log for 'PID=%d'" % ppid
505 sys.stdout.flush()
507 ppid_filenames = [f for f in filenames \
508 if re.search("\.%d\.[0-9]+$" % ppid, f)]
509 # check_sanity won't work with browser wrappers
510 assert check_sanity == False
511 ret |= analyzer.Report(ppid_filenames, testcase_name)
512 print "====================================================="
513 sys.stdout.flush()
515 if ret != 0:
516 print ""
517 print "The Valgrind reports are grouped by test names."
518 print "Each test has its PID printed in the log when the test was run"
519 print "and at the beginning of its Valgrind report."
520 print "Hint: you can search for the reports by Ctrl+F -> `=#`"
521 sys.stdout.flush()
523 return ret
526 # TODO(timurrrr): Split into a separate file.
527 class Memcheck(ValgrindTool):
528 """Memcheck
529 Dynamic memory error detector for Linux & Mac
531 http://valgrind.org/info/tools.html#memcheck
534 def __init__(self):
535 super(Memcheck, self).__init__()
536 self.RegisterOptionParserHook(Memcheck.ExtendOptionParser)
538 def ToolName(self):
539 return "memcheck"
541 def ExtendOptionParser(self, parser):
542 parser.add_option("--leak-check", "--leak_check", type="string",
543 default="yes", # --leak-check=yes is equivalent of =full
544 help="perform leak checking at the end of the run")
545 parser.add_option("", "--show_all_leaks", action="store_true",
546 default=False,
547 help="also show less blatant leaks")
548 parser.add_option("", "--track_origins", action="store_true",
549 default=False,
550 help="Show whence uninitialized bytes came. 30% slower.")
552 def ToolSpecificFlags(self):
553 ret = ["--gen-suppressions=all", "--demangle=no"]
554 ret += ["--leak-check=%s" % self._options.leak_check]
556 if self._options.show_all_leaks:
557 ret += ["--show-reachable=yes"]
558 else:
559 ret += ["--show-possibly-lost=no"]
561 if self._options.track_origins:
562 ret += ["--track-origins=yes"]
564 # TODO(glider): this is a temporary workaround for http://crbug.com/51716
565 # Let's see whether it helps.
566 if common.IsMac():
567 ret += ["--smc-check=all"]
569 return ret
571 def CreateAnalyzer(self):
572 use_gdb = common.IsMac()
573 return memcheck_analyze.MemcheckAnalyzer(self._source_dir,
574 self._options.show_all_leaks,
575 use_gdb=use_gdb)
577 def Analyze(self, check_sanity=False):
578 ret = self.GetAnalyzeResults(check_sanity)
580 if ret != 0:
581 logging.info("Please see http://dev.chromium.org/developers/how-tos/"
582 "using-valgrind for the info on Memcheck/Valgrind")
583 return ret
586 class PinTool(BaseTool):
587 """Abstract class for running PIN tools.
589 Always subclass this and implement ToolSpecificFlags() and
590 ExtendOptionParser() for tool-specific stuff.
592 def PrepareForTest(self):
593 pass
595 def ToolSpecificFlags(self):
596 raise NotImplementedError, "This method should be implemented " \
597 "in the tool-specific subclass"
599 def ToolCommand(self):
600 """Get the PIN command to run."""
602 # Construct the PIN command.
603 pin_cmd = os.getenv("PIN_COMMAND")
604 if not pin_cmd:
605 raise RuntimeError, "Please set PIN_COMMAND environment variable " \
606 "with the path to pin.exe"
607 proc = pin_cmd.split(" ")
609 proc += self.ToolSpecificFlags()
611 # The PIN command is constructed.
613 # PIN requires -- to separate PIN flags from the executable name.
614 # self._args begins with the exe to be run.
615 proc += ["--"]
617 proc += self._args
618 return proc
621 class ThreadSanitizerBase(object):
622 """ThreadSanitizer
623 Dynamic data race detector for Linux, Mac and Windows.
625 http://code.google.com/p/data-race-test/wiki/ThreadSanitizer
627 Since TSan works on both Valgrind (Linux, Mac) and PIN (Windows), we need
628 to have multiple inheritance
631 INFO_MESSAGE="Please see http://dev.chromium.org/developers/how-tos/" \
632 "using-valgrind/threadsanitizer for the info on " \
633 "ThreadSanitizer"
635 def __init__(self):
636 super(ThreadSanitizerBase, self).__init__()
637 self.RegisterOptionParserHook(ThreadSanitizerBase.ExtendOptionParser)
639 def ToolName(self):
640 return "tsan"
642 def UseXML(self):
643 return False
645 def SelfContained(self):
646 return True
648 def ExtendOptionParser(self, parser):
649 parser.add_option("", "--hybrid", default="no",
650 dest="hybrid",
651 help="Finds more data races, may give false positive "
652 "reports unless the code is annotated")
653 parser.add_option("", "--announce-threads", default="yes",
654 dest="announce_threads",
655 help="Show the the stack traces of thread creation")
656 parser.add_option("", "--free-is-write", default="no",
657 dest="free_is_write",
658 help="Treat free()/operator delete as memory write. "
659 "This helps finding more data races, but (currently) "
660 "this may give false positive reports on std::string "
661 "internals, see http://code.google.com/p/data-race-test"
662 "/issues/detail?id=40")
664 def EvalBoolFlag(self, flag_value):
665 if (flag_value in ["1", "true", "yes"]):
666 return True
667 elif (flag_value in ["0", "false", "no"]):
668 return False
669 raise RuntimeError, "Can't parse flag value (%s)" % flag_value
671 def ToolSpecificFlags(self):
672 ret = []
674 ignore_files = ["ignores.txt"]
675 for platform_suffix in common.PlatformNames():
676 ignore_files.append("ignores_%s.txt" % platform_suffix)
677 for ignore_file in ignore_files:
678 fullname = os.path.join(self._source_dir,
679 "tools", "valgrind", "tsan", ignore_file)
680 if os.path.exists(fullname):
681 fullname = common.NormalizeWindowsPath(fullname)
682 ret += ["--ignore=%s" % fullname]
684 # This should shorten filepaths for local builds.
685 ret += ["--file-prefix-to-cut=%s/" % self._source_dir]
687 # This should shorten filepaths on bots.
688 ret += ["--file-prefix-to-cut=build/src/"]
689 ret += ["--file-prefix-to-cut=out/Release/../../"]
691 # This should shorten filepaths for functions intercepted in TSan.
692 ret += ["--file-prefix-to-cut=scripts/tsan/tsan/"]
693 ret += ["--file-prefix-to-cut=src/tsan/tsan/"]
695 ret += ["--gen-suppressions=true"]
697 if self.EvalBoolFlag(self._options.hybrid):
698 ret += ["--hybrid=yes"] # "no" is the default value for TSAN
700 if self.EvalBoolFlag(self._options.announce_threads):
701 ret += ["--announce-threads"]
703 if self.EvalBoolFlag(self._options.free_is_write):
704 ret += ["--free-is-write=yes"]
705 else:
706 ret += ["--free-is-write=no"]
709 # --show-pc flag is needed for parsing the error logs on Darwin.
710 if platform_suffix == 'mac':
711 ret += ["--show-pc=yes"]
712 ret += ["--show-pid=no"]
714 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
715 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
716 for bc in boring_callers:
717 ret += ["--cut_stack_below=%s" % bc]
719 return ret
722 class ThreadSanitizerPosix(ThreadSanitizerBase, ValgrindTool):
723 def ToolSpecificFlags(self):
724 proc = ThreadSanitizerBase.ToolSpecificFlags(self)
725 # The -v flag is needed for printing the list of used suppressions and
726 # obtaining addresses for loaded shared libraries on Mac.
727 proc += ["-v"]
728 return proc
730 def CreateAnalyzer(self):
731 use_gdb = common.IsMac()
732 return tsan_analyze.TsanAnalyzer(use_gdb)
734 def Analyze(self, check_sanity=False):
735 ret = self.GetAnalyzeResults(check_sanity)
737 if ret != 0:
738 logging.info(self.INFO_MESSAGE)
739 return ret
742 class ThreadSanitizerWindows(ThreadSanitizerBase, PinTool):
744 def __init__(self):
745 super(ThreadSanitizerWindows, self).__init__()
746 self.RegisterOptionParserHook(ThreadSanitizerWindows.ExtendOptionParser)
748 def ExtendOptionParser(self, parser):
749 parser.add_option("", "--suppressions", default=[],
750 action="append",
751 help="path to TSan suppression file")
754 def ToolSpecificFlags(self):
755 add_env = {
756 "CHROME_ALLOCATOR" : "WINHEAP",
758 for k,v in add_env.iteritems():
759 logging.info("export %s=%s", k, v)
760 os.putenv(k, v)
762 proc = ThreadSanitizerBase.ToolSpecificFlags(self)
763 # On PIN, ThreadSanitizer has its own suppression mechanism
764 # and --log-file flag which work exactly on Valgrind.
765 suppression_count = 0
766 for suppression_file in self._options.suppressions:
767 if os.path.exists(suppression_file):
768 suppression_count += 1
769 suppression_file = common.NormalizeWindowsPath(suppression_file)
770 proc += ["--suppressions=%s" % suppression_file]
772 if not suppression_count:
773 logging.warning("WARNING: NOT USING SUPPRESSIONS!")
775 logfilename = self.log_dir + "/tsan.%p"
776 proc += ["--log-file=" + common.NormalizeWindowsPath(logfilename)]
778 # TODO(timurrrr): Add flags for Valgrind trace children analog when we
779 # start running complex tests (e.g. UI) under TSan/Win.
781 return proc
783 def Analyze(self, check_sanity=False):
784 filenames = glob.glob(self.log_dir + "/tsan.*")
785 analyzer = tsan_analyze.TsanAnalyzer()
786 ret = analyzer.Report(filenames, None, check_sanity)
787 if ret != 0:
788 logging.info(self.INFO_MESSAGE)
789 return ret
792 class DrMemory(BaseTool):
793 """Dr.Memory
794 Dynamic memory error detector for Windows.
796 http://dev.chromium.org/developers/how-tos/using-drmemory
797 It is not very mature at the moment, some things might not work properly.
800 def __init__(self, full_mode, pattern_mode):
801 super(DrMemory, self).__init__()
802 self.full_mode = full_mode
803 self.pattern_mode = pattern_mode
804 self.RegisterOptionParserHook(DrMemory.ExtendOptionParser)
806 def ToolName(self):
807 return "drmemory"
809 def ExtendOptionParser(self, parser):
810 parser.add_option("", "--suppressions", default=[],
811 action="append",
812 help="path to a drmemory suppression file")
813 parser.add_option("", "--follow_python", action="store_true",
814 default=False, dest="follow_python",
815 help="Monitor python child processes. If off, neither "
816 "python children nor any children of python children "
817 "will be monitored.")
818 parser.add_option("", "--indirect", action="store_true",
819 default=False,
820 help="set BROWSER_WRAPPER rather than "
821 "running Dr. Memory directly on the harness")
822 parser.add_option("", "--indirect_webkit_layout", action="store_true",
823 default=False,
824 help="set --wrapper rather than running valgrind "
825 "directly.")
826 parser.add_option("", "--use_debug", action="store_true",
827 default=False, dest="use_debug",
828 help="Run Dr. Memory debug build")
829 parser.add_option("", "--trace_children", action="store_true",
830 default=True,
831 help="TODO: default value differs from Valgrind")
833 def ToolCommand(self):
834 """Get the tool command to run."""
835 # WINHEAP is what Dr. Memory supports as there are issues w/ both
836 # jemalloc (https://github.com/DynamoRIO/drmemory/issues/320) and
837 # tcmalloc (https://github.com/DynamoRIO/drmemory/issues/314)
838 add_env = {
839 "CHROME_ALLOCATOR" : "WINHEAP",
840 "JSIMD_FORCEMMX" : "1", # https://github.com/DynamoRIO/drmemory/issues/540
842 for k,v in add_env.iteritems():
843 logging.info("export %s=%s", k, v)
844 os.putenv(k, v)
846 drmem_cmd = os.getenv("DRMEMORY_COMMAND")
847 if not drmem_cmd:
848 raise RuntimeError, "Please set DRMEMORY_COMMAND environment variable " \
849 "with the path to drmemory.exe"
850 proc = drmem_cmd.split(" ")
852 # By default, don't run python (this will exclude python's children as well)
853 # to reduce runtime. We're not really interested in spending time finding
854 # bugs in the python implementation.
855 # With file-based config we must update the file every time, and
856 # it will affect simultaneous drmem uses by this user. While file-based
857 # config has many advantages, here we may want this-instance-only
858 # (https://github.com/DynamoRIO/drmemory/issues/334).
859 drconfig_cmd = [ proc[0].replace("drmemory.exe", "drconfig.exe") ]
860 drconfig_cmd += ["-quiet"] # suppress errors about no 64-bit libs
861 run_drconfig = True
862 if self._options.follow_python:
863 logging.info("Following python children")
864 # -unreg fails if not already registered so query for that first
865 query_cmd = drconfig_cmd + ["-isreg", "python.exe"]
866 query_proc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE,
867 shell=True)
868 (query_out, query_err) = query_proc.communicate()
869 if re.search("exe not registered", query_out):
870 run_drconfig = False # all set
871 else:
872 drconfig_cmd += ["-unreg", "python.exe"]
873 else:
874 logging.info("Excluding python children")
875 drconfig_cmd += ["-reg", "python.exe", "-norun"]
876 if run_drconfig:
877 drconfig_retcode = common.RunSubprocess(drconfig_cmd, self._timeout)
878 if drconfig_retcode:
879 logging.error("Configuring whether to follow python children failed " \
880 "with %d.", drconfig_retcode)
881 raise RuntimeError, "Configuring python children failed "
883 suppression_count = 0
884 supp_files = self._options.suppressions
885 if self.full_mode:
886 supp_files += [s.replace(".txt", "_full.txt") for s in supp_files]
887 for suppression_file in supp_files:
888 if os.path.exists(suppression_file):
889 suppression_count += 1
890 proc += ["-suppress", common.NormalizeWindowsPath(suppression_file)]
892 if not suppression_count:
893 logging.warning("WARNING: NOT USING SUPPRESSIONS!")
895 # Un-comment to dump Dr.Memory events on error
896 #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"]
898 # Un-comment and comment next line to debug Dr.Memory
899 #proc += ["-dr_ops", "-no_hide"]
900 #proc += ["-dr_ops", "-msgbox_mask", "-dr_ops", "15"]
901 #Proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "15"]
902 # Ensure we see messages about Dr. Memory crashing!
903 proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "12"]
905 if self._options.use_debug:
906 proc += ["-debug"]
908 proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)]
910 if self.log_parent_dir:
911 # gpu process on Windows Vista+ runs at Low Integrity and can only
912 # write to certain directories (http://crbug.com/119131)
913 symcache_dir = os.path.join(self.log_parent_dir, "drmemory.symcache")
914 elif self._options.build_dir:
915 # The other case is only possible with -t cmdline.
916 # Anyways, if we omit -symcache_dir the -logdir's value is used which
917 # should be fine.
918 symcache_dir = os.path.join(self._options.build_dir, "drmemory.symcache")
919 if symcache_dir:
920 if not os.path.exists(symcache_dir):
921 try:
922 os.mkdir(symcache_dir)
923 except OSError:
924 logging.warning("Can't create symcache dir?")
925 if os.path.exists(symcache_dir):
926 proc += ["-symcache_dir", common.NormalizeWindowsPath(symcache_dir)]
928 # Use -no_summary to suppress DrMemory's summary and init-time
929 # notifications. We generate our own with drmemory_analyze.py.
930 proc += ["-batch", "-no_summary"]
932 # Un-comment to disable interleaved output. Will also suppress error
933 # messages normally printed to stderr.
934 #proc += ["-quiet", "-no_results_to_stderr"]
936 proc += ["-callstack_max_frames", "40"]
938 # disable leak scan for now
939 proc += ["-no_count_leaks", "-no_leak_scan"]
941 # crbug.com/413215, no heap mismatch check for Windows release build binary
942 if common.IsWindows() and "Release" in self._options.build_dir:
943 proc += ["-no_check_delete_mismatch"]
945 # make callstacks easier to read
946 proc += ["-callstack_srcfile_prefix",
947 "build\\src,chromium\\src,crt_build\\self_x86"]
948 proc += ["-callstack_modname_hide",
949 "*drmemory*,chrome.dll"]
951 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
952 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
953 proc += ["-callstack_truncate_below", ",".join(boring_callers)]
955 if self.pattern_mode:
956 proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"]
957 elif not self.full_mode:
958 proc += ["-light"]
960 proc += self._tool_flags
962 # Dr.Memory requires -- to separate tool flags from the executable name.
963 proc += ["--"]
965 if self._options.indirect or self._options.indirect_webkit_layout:
966 # TODO(timurrrr): reuse for TSan on Windows
967 wrapper_path = os.path.join(self._source_dir,
968 "tools", "valgrind", "browser_wrapper_win.py")
969 wrapper = " ".join(["python", wrapper_path] + proc)
970 self.CreateBrowserWrapper(wrapper)
971 logging.info("browser wrapper = " + " ".join(proc))
972 if self._options.indirect_webkit_layout:
973 proc = self._args
974 # Layout tests want forward slashes.
975 wrapper = wrapper.replace('\\', '/')
976 proc += ["--wrapper", wrapper]
977 return proc
978 else:
979 proc = []
981 # Note that self._args begins with the name of the exe to be run.
982 self._args[0] = common.NormalizeWindowsPath(self._args[0])
983 proc += self._args
984 return proc
986 def CreateBrowserWrapper(self, command):
987 os.putenv("BROWSER_WRAPPER", command)
989 def Analyze(self, check_sanity=False):
990 # Use one analyzer for all the log files to avoid printing duplicate reports
992 # TODO(timurrrr): unify this with Valgrind and other tools when we have
993 # https://github.com/DynamoRIO/drmemory/issues/684
994 analyzer = drmemory_analyze.DrMemoryAnalyzer()
996 ret = 0
997 if not self._options.indirect and not self._options.indirect_webkit_layout:
998 filenames = glob.glob(self.log_dir + "/*/results.txt")
1000 ret = analyzer.Report(filenames, None, check_sanity)
1001 else:
1002 testcases = glob.glob(self.log_dir + "/testcase.*.logs")
1003 # If we have browser wrapper, the per-test logdirs are named as
1004 # "testcase.wrapper_PID.name".
1005 # Let's extract the list of wrapper_PIDs and name it ppids.
1006 # NOTE: ppids may contain '_', i.e. they are not ints!
1007 ppids = set([f.split(".")[-2] for f in testcases])
1009 for ppid in ppids:
1010 testcase_name = None
1011 try:
1012 f = open("%s/testcase.%s.name" % (self.log_dir, ppid))
1013 testcase_name = f.read().strip()
1014 f.close()
1015 except IOError:
1016 pass
1017 print "====================================================="
1018 print " Below is the report for drmemory wrapper PID=%s." % ppid
1019 if testcase_name:
1020 print " It was used while running the `%s` test." % testcase_name
1021 else:
1022 # TODO(timurrrr): hm, the PID line is suppressed on Windows...
1023 print " You can find the corresponding test"
1024 print " by searching the above log for 'PID=%s'" % ppid
1025 sys.stdout.flush()
1026 ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" %
1027 (self.log_dir, ppid))
1028 ret |= analyzer.Report(ppid_filenames, testcase_name, False)
1029 print "====================================================="
1030 sys.stdout.flush()
1032 logging.info("Please see http://dev.chromium.org/developers/how-tos/"
1033 "using-drmemory for the info on Dr. Memory")
1034 return ret
1037 # RaceVerifier support. See
1038 # http://code.google.com/p/data-race-test/wiki/RaceVerifier for more details.
1039 class ThreadSanitizerRV1Analyzer(tsan_analyze.TsanAnalyzer):
1040 """ TsanAnalyzer that saves race reports to a file. """
1042 TMP_FILE = "rvlog.tmp"
1044 def __init__(self, source_dir, use_gdb):
1045 super(ThreadSanitizerRV1Analyzer, self).__init__(use_gdb)
1046 self.out = open(self.TMP_FILE, "w")
1048 def Report(self, files, testcase, check_sanity=False):
1049 reports = self.GetReports(files)
1050 for report in reports:
1051 print >>self.out, report
1052 if len(reports) > 0:
1053 logging.info("RaceVerifier pass 1 of 2, found %i reports" % len(reports))
1054 return -1
1055 return 0
1057 def CloseOutputFile(self):
1058 self.out.close()
1061 class ThreadSanitizerRV1Mixin(object):
1062 """RaceVerifier first pass.
1064 Runs ThreadSanitizer as usual, but hides race reports and collects them in a
1065 temporary file"""
1067 def __init__(self):
1068 super(ThreadSanitizerRV1Mixin, self).__init__()
1069 self.RegisterOptionParserHook(ThreadSanitizerRV1Mixin.ExtendOptionParser)
1071 def ExtendOptionParser(self, parser):
1072 parser.set_defaults(hybrid="yes")
1074 def CreateAnalyzer(self):
1075 use_gdb = common.IsMac()
1076 self.analyzer = ThreadSanitizerRV1Analyzer(self._source_dir, use_gdb)
1077 return self.analyzer
1079 def Cleanup(self):
1080 super(ThreadSanitizerRV1Mixin, self).Cleanup()
1081 self.analyzer.CloseOutputFile()
1084 class ThreadSanitizerRV2Mixin(object):
1085 """RaceVerifier second pass."""
1087 def __init__(self):
1088 super(ThreadSanitizerRV2Mixin, self).__init__()
1089 self.RegisterOptionParserHook(ThreadSanitizerRV2Mixin.ExtendOptionParser)
1091 def ExtendOptionParser(self, parser):
1092 parser.add_option("", "--race-verifier-sleep-ms",
1093 dest="race_verifier_sleep_ms", default=10,
1094 help="duration of RaceVerifier delays")
1096 def ToolSpecificFlags(self):
1097 proc = super(ThreadSanitizerRV2Mixin, self).ToolSpecificFlags()
1098 proc += ['--race-verifier=%s' % ThreadSanitizerRV1Analyzer.TMP_FILE,
1099 '--race-verifier-sleep-ms=%d' %
1100 int(self._options.race_verifier_sleep_ms)]
1101 return proc
1103 def Cleanup(self):
1104 super(ThreadSanitizerRV2Mixin, self).Cleanup()
1105 os.unlink(ThreadSanitizerRV1Analyzer.TMP_FILE)
1108 class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin, ThreadSanitizerPosix):
1109 pass
1112 class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin, ThreadSanitizerPosix):
1113 pass
1116 class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin,
1117 ThreadSanitizerWindows):
1118 pass
1121 class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin,
1122 ThreadSanitizerWindows):
1123 pass
1126 class RaceVerifier(object):
1127 """Runs tests under RaceVerifier/Valgrind."""
1129 MORE_INFO_URL = "http://code.google.com/p/data-race-test/wiki/RaceVerifier"
1131 def RV1Factory(self):
1132 if common.IsWindows():
1133 return ThreadSanitizerRV1Windows()
1134 else:
1135 return ThreadSanitizerRV1Posix()
1137 def RV2Factory(self):
1138 if common.IsWindows():
1139 return ThreadSanitizerRV2Windows()
1140 else:
1141 return ThreadSanitizerRV2Posix()
1143 def ToolName(self):
1144 return "tsan"
1146 def Main(self, args, check_sanity, min_runtime_in_seconds):
1147 logging.info("Running a TSan + RaceVerifier test. For more information, " +
1148 "see " + self.MORE_INFO_URL)
1149 cmd1 = self.RV1Factory()
1150 ret = cmd1.Main(args, check_sanity, min_runtime_in_seconds)
1151 # Verify race reports, if there are any.
1152 if ret == -1:
1153 logging.info("Starting pass 2 of 2. Running the same binary in " +
1154 "RaceVerifier mode to confirm possible race reports.")
1155 logging.info("For more information, see " + self.MORE_INFO_URL)
1156 cmd2 = self.RV2Factory()
1157 ret = cmd2.Main(args, check_sanity, min_runtime_in_seconds)
1158 else:
1159 logging.info("No reports, skipping RaceVerifier second pass")
1160 logging.info("Please see " + self.MORE_INFO_URL + " for more information " +
1161 "on RaceVerifier")
1162 return ret
1164 def Run(self, args, module, min_runtime_in_seconds=0):
1165 return self.Main(args, False, min_runtime_in_seconds)
1168 class ToolFactory:
1169 def Create(self, tool_name):
1170 if tool_name == "memcheck":
1171 return Memcheck()
1172 if tool_name == "tsan":
1173 if common.IsWindows():
1174 return ThreadSanitizerWindows()
1175 else:
1176 return ThreadSanitizerPosix()
1177 if tool_name == "drmemory" or tool_name == "drmemory_light":
1178 # TODO(timurrrr): remove support for "drmemory" when buildbots are
1179 # switched to drmemory_light OR make drmemory==drmemory_full the default
1180 # mode when the tool is mature enough.
1181 return DrMemory(False, False)
1182 if tool_name == "drmemory_full":
1183 return DrMemory(True, False)
1184 if tool_name == "drmemory_pattern":
1185 return DrMemory(False, True)
1186 if tool_name == "tsan_rv":
1187 return RaceVerifier()
1188 try:
1189 platform_name = common.PlatformNames()[0]
1190 except common.NotImplementedError:
1191 platform_name = sys.platform + "(Unknown)"
1192 raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name,
1193 platform_name)
1195 def CreateTool(tool):
1196 return ToolFactory().Create(tool)