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
23 import drmemory_analyze
24 import memcheck_analyze
27 class BaseTool(object):
28 """Abstract class for running Valgrind-, PIN-based and other dynamic
31 Always subclass this and implement ToolCommand with framework- and
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")
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
58 "G_SLICE" : "always-malloc",
59 "NSS_DISABLE_UNLOAD" : "1",
60 "NSS_DISABLE_ARENA_FREE_LIST" : "1",
61 "GTEST_DEATH_TEST_USE_FORK": "1",
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 "
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",
95 help="show how long each test takes")
96 self
._parser
.add_option("", "--ignore_exit_code", action
="store_true",
98 help="ignore exit code of the test "
99 "(e.g. test failures)")
100 self
._parser
.add_option("", "--keep_logs", action
="store_true",
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
= []
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] == "-":
130 if self
._parser
.has_option(arg
.split("=")[0]):
133 self
._tool
_flags
+= [arg
]
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")
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"
169 # You may override it in the tool-specific subclass
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
)
186 logging
.error("Analyze failed.")
187 logging
.info("Search the log for '[ERROR]' to see the error reports.")
188 return analyze_retcode
191 if self
._ignore
_exit
_code
:
192 logging
.info("Test execution failed, but the exit code is ignored.")
194 logging
.error("Test execution failed.")
197 logging
.info("Test execution completed successfully.")
199 if not analyze_retcode
:
200 logging
.info("Analysis completed successfully.")
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()
209 retcode
= self
.RunTestsAndAnalyze(check_sanity
)
210 shutil
.rmtree(self
.temp_dir
, ignore_errors
=True)
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
)
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.
247 super(ValgrindTool
, self
).__init
__()
248 self
.RegisterOptionParserHook(ValgrindTool
.ExtendOptionParser
)
251 # Override if tool prefers nonxml output
254 def SelfContained(self
):
255 # Returns true iff the tool is distibuted as a self-contained
256 # .sh script (e.g. ThreadSanitizer)
259 def ExtendOptionParser(self
, parser
):
260 parser
.add_option("", "--suppressions", default
=[],
262 help="path to a valgrind suppression file")
263 parser
.add_option("", "--indirect", action
="store_true",
265 help="set BROWSER_WRAPPER rather than "
266 "running valgrind directly")
267 parser
.add_option("", "--indirect_webkit_layout", action
="store_true",
269 help="set --wrapper rather than running Dr. Memory "
271 parser
.add_option("", "--trace_children", action
="store_true",
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",
279 help="Generate .dSYM file on Mac if needed. Slow!")
281 def Setup(self
, args
):
282 if not BaseTool
.Setup(self
, args
):
285 self
.PrepareForTestMac()
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
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
,
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
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
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
)
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
)
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
]
360 if 'CHROME_VALGRIND' in os
.environ
:
361 path
= os
.path
.join(os
.environ
['CHROME_VALGRIND'], "bin", "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"
391 proc
+= ["--xml=yes", "--xml-file=" + logfilename
]
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)
408 proc
.append("--wrapper")
412 if self
._options
.indirect
:
413 wrapper
= self
.CreateBrowserWrapper(proc
)
414 os
.environ
["BROWSER_WRAPPER"] = wrapper
415 logging
.info('export BROWSER_WRAPPER=' + wrapper
)
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
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.",
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')
447 # Webkit layout_tests pass the URL as the first line of stdin.
448 f
.write('tee $TESTNAME_FILE | %s "$@"\n' % command
)
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'
460 '%s "$@"\n' % command
)
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()
482 # Fast path - no browser wrapper was set.
483 return analyzer
.Report(filenames
, None, check_sanity
)
489 f
= open(self
.log_dir
+ ("/testcase.%d.name" % ppid
))
490 testcase_name
= f
.read().strip()
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
):]
498 print "====================================================="
499 print " Below is the report for valgrind wrapper PID=%d." % ppid
501 print " It was used while running the `%s` test." % testcase_name
503 print " You can find the corresponding test"
504 print " by searching the above log for 'PID=%d'" % ppid
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 "====================================================="
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 -> `=#`"
526 # TODO(timurrrr): Split into a separate file.
527 class Memcheck(ValgrindTool
):
529 Dynamic memory error detector for Linux & Mac
531 http://valgrind.org/info/tools.html#memcheck
535 super(Memcheck
, self
).__init
__()
536 self
.RegisterOptionParserHook(Memcheck
.ExtendOptionParser
)
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",
547 help="also show less blatant leaks")
548 parser
.add_option("", "--track_origins", action
="store_true",
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"]
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.
567 ret
+= ["--smc-check=all"]
571 def CreateAnalyzer(self
):
572 use_gdb
= common
.IsMac()
573 return memcheck_analyze
.MemcheckAnalyzer(self
._source
_dir
,
574 self
._options
.show_all_leaks
,
577 def Analyze(self
, check_sanity
=False):
578 ret
= self
.GetAnalyzeResults(check_sanity
)
581 logging
.info("Please see http://dev.chromium.org/developers/how-tos/"
582 "using-valgrind for the info on Memcheck/Valgrind")
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
):
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")
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.
621 class ThreadSanitizerBase(object):
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 " \
636 super(ThreadSanitizerBase
, self
).__init
__()
637 self
.RegisterOptionParserHook(ThreadSanitizerBase
.ExtendOptionParser
)
645 def SelfContained(self
):
648 def ExtendOptionParser(self
, parser
):
649 parser
.add_option("", "--hybrid", default
="no",
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"]):
667 elif (flag_value
in ["0", "false", "no"]):
669 raise RuntimeError, "Can't parse flag value (%s)" % flag_value
671 def ToolSpecificFlags(self
):
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"]
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
]
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.
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
)
738 logging
.info(self
.INFO_MESSAGE
)
742 class ThreadSanitizerWindows(ThreadSanitizerBase
, PinTool
):
745 super(ThreadSanitizerWindows
, self
).__init
__()
746 self
.RegisterOptionParserHook(ThreadSanitizerWindows
.ExtendOptionParser
)
748 def ExtendOptionParser(self
, parser
):
749 parser
.add_option("", "--suppressions", default
=[],
751 help="path to TSan suppression file")
754 def ToolSpecificFlags(self
):
756 "CHROME_ALLOCATOR" : "WINHEAP",
758 for k
,v
in add_env
.iteritems():
759 logging
.info("export %s=%s", 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.
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
)
788 logging
.info(self
.INFO_MESSAGE
)
792 class DrMemory(BaseTool
):
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
)
809 def ExtendOptionParser(self
, parser
):
810 parser
.add_option("", "--suppressions", default
=[],
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",
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",
824 help="set --wrapper rather than running valgrind "
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",
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)
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
)
846 drmem_cmd
= os
.getenv("DRMEMORY_COMMAND")
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
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
,
868 (query_out
, query_err
) = query_proc
.communicate()
869 if re
.search("exe not registered", query_out
):
870 run_drconfig
= False # all set
872 drconfig_cmd
+= ["-unreg", "python.exe"]
874 logging
.info("Excluding python children")
875 drconfig_cmd
+= ["-reg", "python.exe", "-norun"]
877 drconfig_retcode
= common
.RunSubprocess(drconfig_cmd
, self
._timeout
)
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
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
:
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
918 symcache_dir
= os
.path
.join(self
._options
.build_dir
, "drmemory.symcache")
920 if not os
.path
.exists(symcache_dir
):
922 os
.mkdir(symcache_dir
)
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
:
960 proc
+= self
._tool
_flags
962 # Dr.Memory requires -- to separate tool flags from the executable name.
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
:
974 # Layout tests want forward slashes.
975 wrapper
= wrapper
.replace('\\', '/')
976 proc
+= ["--wrapper", wrapper
]
981 # Note that self._args begins with the name of the exe to be run.
982 self
._args
[0] = common
.NormalizeWindowsPath(self
._args
[0])
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()
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
)
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
])
1010 testcase_name
= None
1012 f
= open("%s/testcase.%s.name" % (self
.log_dir
, ppid
))
1013 testcase_name
= f
.read().strip()
1017 print "====================================================="
1018 print " Below is the report for drmemory wrapper PID=%s." % ppid
1020 print " It was used while running the `%s` test." % testcase_name
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
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 "====================================================="
1032 logging
.info("Please see http://dev.chromium.org/developers/how-tos/"
1033 "using-drmemory for the info on Dr. Memory")
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
))
1057 def CloseOutputFile(self
):
1061 class ThreadSanitizerRV1Mixin(object):
1062 """RaceVerifier first pass.
1064 Runs ThreadSanitizer as usual, but hides race reports and collects them in a
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
1080 super(ThreadSanitizerRV1Mixin
, self
).Cleanup()
1081 self
.analyzer
.CloseOutputFile()
1084 class ThreadSanitizerRV2Mixin(object):
1085 """RaceVerifier second pass."""
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
)]
1104 super(ThreadSanitizerRV2Mixin
, self
).Cleanup()
1105 os
.unlink(ThreadSanitizerRV1Analyzer
.TMP_FILE
)
1108 class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin
, ThreadSanitizerPosix
):
1112 class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin
, ThreadSanitizerPosix
):
1116 class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin
,
1117 ThreadSanitizerWindows
):
1121 class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin
,
1122 ThreadSanitizerWindows
):
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()
1135 return ThreadSanitizerRV1Posix()
1137 def RV2Factory(self
):
1138 if common
.IsWindows():
1139 return ThreadSanitizerRV2Windows()
1141 return ThreadSanitizerRV2Posix()
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.
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
)
1159 logging
.info("No reports, skipping RaceVerifier second pass")
1160 logging
.info("Please see " + self
.MORE_INFO_URL
+ " for more information " +
1164 def Run(self
, args
, module
, min_runtime_in_seconds
=0):
1165 return self
.Main(args
, False, min_runtime_in_seconds
)
1169 def Create(self
, tool_name
):
1170 if tool_name
== "memcheck":
1172 if tool_name
== "tsan":
1173 if common
.IsWindows():
1174 return ThreadSanitizerWindows()
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()
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
,
1195 def CreateTool(tool
):
1196 return ToolFactory().Create(tool
)