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. 4 5 """Runs an exe through Valgrind and puts the intermediate files in a 6 directory. 7 """ 8 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 20 21 import common 22 23 import drmemory_analyze 24 import memcheck_analyze 25 import tsan_analyze 26 27 class BaseTool(object): 28 """Abstract class for running Valgrind-, PIN-based and other dynamic 29 error detector tools. 30 31 Always subclass this and implement ToolCommand with framework- and 32 tool-specific stuff. 33 """ 34 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) 41 # 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", 62 } 63 64 def ToolName(self): 65 raise NotImplementedError, "This method should be implemented " \ 66 "in the tool-specific subclass" 67 68 def Analyze(self, check_sanity=False): 69 raise NotImplementedError, "This method should be implemented " \ 70 "in the tool-specific subclass" 71 72 def RegisterOptionParserHook(self, hook): 73 # Frameworks and tools can add their own flags to the parser. 74 self.option_parser_hooks.append(hook) 75 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.") 107 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) 113 114 def ParseArgv(self, args): 115 self.CreateOptionParser() 116 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 = [] 121 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 125 """ 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:] 135 136 if len(args) > 0: 137 known_args += args 138 139 self._options, self._args = self._parser.parse_args(known_args) 140 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) 150 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") 158 159 return True 160 161 def Setup(self, args): 162 return self.ParseArgv(args) 163 164 def ToolCommand(self): 165 raise NotImplementedError, "This method should be implemented " \ 166 "in the tool-specific subclass" 167 168 def Cleanup(self): 169 # You may override it in the tool-specific subclass 170 pass 171 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) 180 181 def RunTestsAndAnalyze(self, check_sanity): 182 exec_retcode = self.Execute() 183 analyze_retcode = self.Analyze(check_sanity) 184 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 189 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.") 198 199 if not analyze_retcode: 200 logging.info("Analysis completed successfully.") 201 202 return 0 203 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 228 229 def Run(self, args, module, min_runtime_in_seconds=0): 230 MODULES_TO_SANITY_CHECK = ["base"] 231 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 = [] 235 236 check_sanity = module in MODULES_TO_SANITY_CHECK 237 return self.Main(args, check_sanity, min_runtime_in_seconds) 238 239 240 class ValgrindTool(BaseTool): 241 """Abstract class for running Valgrind tools. 242 243 Always subclass this and implement ToolSpecificFlags() and 244 ExtendOptionParser() for tool-specific stuff. 245 """ 246 def __init__(self): 247 super(ValgrindTool, self).__init__() 248 self.RegisterOptionParserHook(ValgrindTool.ExtendOptionParser) 249 250 def UseXML(self): 251 # Override if tool prefers nonxml output 252 return True 253 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 258 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!") 280 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 287 288 def PrepareForTestMac(self): 289 """Runs dsymutil if needed. 290 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. 297 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. 301 """ 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') 307 308 needs_dsymutil = True 309 saved_test_command = None 310 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() 314 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) 330 331 if needs_dsymutil: 332 if self._options.generate_dsym: 333 # Remove the .dSYM bundle if it exists. 334 shutil.rmtree(dsym_bundle, True) 335 336 dsymutil_command = ['dsymutil', test_command] 337 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) 343 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.") 350 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() 355 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] 365 366 proc += ["--num-callers=%i" % int(self._options.num_callers)] 367 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*'"] 376 377 proc += self.ToolSpecificFlags() 378 proc += self._tool_flags 379 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] 385 386 if not suppression_count: 387 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 388 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] 394 395 # The Valgrind command is constructed. 396 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 fron environment") 401 os.environ["CHROME_DEVEL_SANDBOX"] = '' 402 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 411 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 419 420 def ToolSpecificFlags(self): 421 raise NotImplementedError, "This method should be implemented " \ 422 "in the tool-specific subclass" 423 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. 430 """ 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") 435 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') 442 443 f.write('DIR=`dirname $0`\n' 444 'TESTNAME_FILE=$DIR/testcase.$$.name\n\n') 445 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) 461 462 f.close() 463 os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR) 464 return indirect_fname 465 466 def CreateAnalyzer(self): 467 raise NotImplementedError, "This method should be implemented " \ 468 "in the tool-specific subclass" 469 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() + ".*") 473 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)]) 479 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) 484 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() 506 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() 514 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() 522 523 return ret 524 525 526 # TODO(timurrrr): Split into a separate file. 527 class Memcheck(ValgrindTool): 528 """Memcheck 529 Dynamic memory error detector for Linux & Mac 530 531 http://valgrind.org/info/tools.html#memcheck 532 """ 533 534 def __init__(self): 535 super(Memcheck, self).__init__() 536 self.RegisterOptionParserHook(Memcheck.ExtendOptionParser) 537 538 def ToolName(self): 539 return "memcheck" 540 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.") 551 552 def ToolSpecificFlags(self): 553 ret = ["--gen-suppressions=all", "--demangle=no"] 554 ret += ["--leak-check=%s" % self._options.leak_check] 555 556 if self._options.show_all_leaks: 557 ret += ["--show-reachable=yes"] 558 else: 559 ret += ["--show-possibly-lost=no"] 560 561 if self._options.track_origins: 562 ret += ["--track-origins=yes"] 563 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"] 568 569 return ret 570 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) 576 577 def Analyze(self, check_sanity=False): 578 ret = self.GetAnalyzeResults(check_sanity) 579 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 584 585 586 class PinTool(BaseTool): 587 """Abstract class for running PIN tools. 588 589 Always subclass this and implement ToolSpecificFlags() and 590 ExtendOptionParser() for tool-specific stuff. 591 """ 592 def PrepareForTest(self): 593 pass 594 595 def ToolSpecificFlags(self): 596 raise NotImplementedError, "This method should be implemented " \ 597 "in the tool-specific subclass" 598 599 def ToolCommand(self): 600 """Get the PIN command to run.""" 601 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(" ") 608 609 proc += self.ToolSpecificFlags() 610 611 # The PIN command is constructed. 612 613 # PIN requires -- to separate PIN flags from the executable name. 614 # self._args begins with the exe to be run. 615 proc += ["--"] 616 617 proc += self._args 618 return proc 619 620 621 class ThreadSanitizerBase(object): 622 """ThreadSanitizer 623 Dynamic data race detector for Linux, Mac and Windows. 624 625 http://code.google.com/p/data-race-test/wiki/ThreadSanitizer 626 627 Since TSan works on both Valgrind (Linux, Mac) and PIN (Windows), we need 628 to have multiple inheritance 629 """ 630 631 INFO_MESSAGE="Please see http://dev.chromium.org/developers/how-tos/" \ 632 "using-valgrind/threadsanitizer for the info on " \ 633 "ThreadSanitizer" 634 635 def __init__(self): 636 super(ThreadSanitizerBase, self).__init__() 637 self.RegisterOptionParserHook(ThreadSanitizerBase.ExtendOptionParser) 638 639 def ToolName(self): 640 return "tsan" 641 642 def UseXML(self): 643 return False 644 645 def SelfContained(self): 646 return True 647 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") 663 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 670 671 def ToolSpecificFlags(self): 672 ret = [] 673 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] 683 684 # This should shorten filepaths for local builds. 685 ret += ["--file-prefix-to-cut=%s/" % self._source_dir] 686 687 # This should shorten filepaths on bots. 688 ret += ["--file-prefix-to-cut=build/src/"] 689 ret += ["--file-prefix-to-cut=out/Release/../../"] 690 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/"] 694 695 ret += ["--gen-suppressions=true"] 696 697 if self.EvalBoolFlag(self._options.hybrid): 698 ret += ["--hybrid=yes"] # "no" is the default value for TSAN 699 700 if self.EvalBoolFlag(self._options.announce_threads): 701 ret += ["--announce-threads"] 702 703 if self.EvalBoolFlag(self._options.free_is_write): 704 ret += ["--free-is-write=yes"] 705 else: 706 ret += ["--free-is-write=no"] 707 708 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"] 713 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] 718 719 return ret 720 721 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 729 730 def CreateAnalyzer(self): 731 use_gdb = common.IsMac() 732 return tsan_analyze.TsanAnalyzer(use_gdb) 733 734 def Analyze(self, check_sanity=False): 735 ret = self.GetAnalyzeResults(check_sanity) 736 737 if ret != 0: 738 logging.info(self.INFO_MESSAGE) 739 return ret 740 741 742 class ThreadSanitizerWindows(ThreadSanitizerBase, PinTool): 743 744 def __init__(self): 745 super(ThreadSanitizerWindows, self).__init__() 746 self.RegisterOptionParserHook(ThreadSanitizerWindows.ExtendOptionParser) 747 748 def ExtendOptionParser(self, parser): 749 parser.add_option("", "--suppressions", default=[], 750 action="append", 751 help="path to TSan suppression file") 752 753 754 def ToolSpecificFlags(self): 755 add_env = { 756 "CHROME_ALLOCATOR" : "WINHEAP", 757 } 758 for k,v in add_env.iteritems(): 759 logging.info("export %s=%s", k, v) 760 os.putenv(k, v) 761 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] 771 772 if not suppression_count: 773 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 774 775 logfilename = self.log_dir + "/tsan.%p" 776 proc += ["--log-file=" + common.NormalizeWindowsPath(logfilename)] 777 778 # TODO(timurrrr): Add flags for Valgrind trace children analog when we 779 # start running complex tests (e.g. UI) under TSan/Win. 780 781 return proc 782 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 790 791 792 class DrMemory(BaseTool): 793 """Dr.Memory 794 Dynamic memory error detector for Windows. 795 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. 798 """ 799 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) 805 806 def ToolName(self): 807 return "drmemory" 808 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") 832 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 (http://code.google.com/p/drmemory/issues/detail?id=320) and 837 # tcmalloc (http://code.google.com/p/drmemory/issues/detail?id=314) 838 add_env = { 839 "CHROME_ALLOCATOR" : "WINHEAP", 840 "JSIMD_FORCEMMX" : "1", # http://code.google.com/p/drmemory/issues/detail?id=540 841 } 842 for k,v in add_env.iteritems(): 843 logging.info("export %s=%s", k, v) 844 os.putenv(k, v) 845 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(" ") 851 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 # (http://code.google.com/p/drmemory/issues/detail?id=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 " 882 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)] 891 892 if not suppression_count: 893 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 894 895 # Un-comment to dump Dr.Memory events on error 896 #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"] 897 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"] 904 905 if self._options.use_debug: 906 proc += ["-debug"] 907 908 proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)] 909 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)] 927 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"] 931 932 # Un-comment to disable interleaved output. Will also suppress error 933 # messages normally printed to stderr. 934 #proc += ["-quiet", "-no_results_to_stderr"] 935 936 proc += ["-callstack_max_frames", "40"] 937 938 # disable leak scan for now 939 proc += ["-no_count_leaks", "-no_leak_scan"] 940 941 # make callstacks easier to read 942 proc += ["-callstack_srcfile_prefix", 943 "build\\src,chromium\\src,crt_build\\self_x86"] 944 proc += ["-callstack_modname_hide", 945 "*drmemory*,chrome.dll"] 946 947 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False) 948 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .." 949 proc += ["-callstack_truncate_below", ",".join(boring_callers)] 950 951 if self.pattern_mode: 952 proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"] 953 elif not self.full_mode: 954 proc += ["-light"] 955 956 proc += self._tool_flags 957 958 # DrM i#850/851: The new -callstack_use_top_fp_selectively has bugs. 959 proc += ["-no_callstack_use_top_fp_selectively"] 960 961 # Dr.Memory requires -- to separate tool flags from the executable name. 962 proc += ["--"] 963 964 if self._options.indirect or self._options.indirect_webkit_layout: 965 # TODO(timurrrr): reuse for TSan on Windows 966 wrapper_path = os.path.join(self._source_dir, 967 "tools", "valgrind", "browser_wrapper_win.py") 968 wrapper = " ".join(["python", wrapper_path] + proc) 969 self.CreateBrowserWrapper(wrapper) 970 logging.info("browser wrapper = " + " ".join(proc)) 971 if self._options.indirect_webkit_layout: 972 proc = self._args 973 # Layout tests want forward slashes. 974 wrapper = wrapper.replace('\\', '/') 975 proc += ["--wrapper", wrapper] 976 return proc 977 else: 978 proc = [] 979 980 # Note that self._args begins with the name of the exe to be run. 981 self._args[0] = common.NormalizeWindowsPath(self._args[0]) 982 proc += self._args 983 return proc 984 985 def CreateBrowserWrapper(self, command): 986 os.putenv("BROWSER_WRAPPER", command) 987 988 def Analyze(self, check_sanity=False): 989 # Use one analyzer for all the log files to avoid printing duplicate reports 990 # 991 # TODO(timurrrr): unify this with Valgrind and other tools when we have 992 # http://code.google.com/p/drmemory/issues/detail?id=684 993 analyzer = drmemory_analyze.DrMemoryAnalyzer() 994 995 ret = 0 996 if not self._options.indirect and not self._options.indirect_webkit_layout: 997 filenames = glob.glob(self.log_dir + "/*/results.txt") 998 999 ret = analyzer.Report(filenames, None, check_sanity) 1000 else: 1001 testcases = glob.glob(self.log_dir + "/testcase.*.logs") 1002 # If we have browser wrapper, the per-test logdirs are named as 1003 # "testcase.wrapper_PID.name". 1004 # Let's extract the list of wrapper_PIDs and name it ppids. 1005 # NOTE: ppids may contain '_', i.e. they are not ints! 1006 ppids = set([f.split(".")[-2] for f in testcases]) 1007 1008 for ppid in ppids: 1009 testcase_name = None 1010 try: 1011 f = open("%s/testcase.%s.name" % (self.log_dir, ppid)) 1012 testcase_name = f.read().strip() 1013 f.close() 1014 except IOError: 1015 pass 1016 print "=====================================================" 1017 print " Below is the report for drmemory wrapper PID=%s." % ppid 1018 if testcase_name: 1019 print " It was used while running the `%s` test." % testcase_name 1020 else: 1021 # TODO(timurrrr): hm, the PID line is suppressed on Windows... 1022 print " You can find the corresponding test" 1023 print " by searching the above log for 'PID=%s'" % ppid 1024 sys.stdout.flush() 1025 ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" % 1026 (self.log_dir, ppid)) 1027 ret |= analyzer.Report(ppid_filenames, testcase_name, False) 1028 print "=====================================================" 1029 sys.stdout.flush() 1030 1031 logging.info("Please see http://dev.chromium.org/developers/how-tos/" 1032 "using-drmemory for the info on Dr. Memory") 1033 return ret 1034 1035 1036 # RaceVerifier support. See 1037 # http://code.google.com/p/data-race-test/wiki/RaceVerifier for more details. 1038 class ThreadSanitizerRV1Analyzer(tsan_analyze.TsanAnalyzer): 1039 """ TsanAnalyzer that saves race reports to a file. """ 1040 1041 TMP_FILE = "rvlog.tmp" 1042 1043 def __init__(self, source_dir, use_gdb): 1044 super(ThreadSanitizerRV1Analyzer, self).__init__(use_gdb) 1045 self.out = open(self.TMP_FILE, "w") 1046 1047 def Report(self, files, testcase, check_sanity=False): 1048 reports = self.GetReports(files) 1049 for report in reports: 1050 print >>self.out, report 1051 if len(reports) > 0: 1052 logging.info("RaceVerifier pass 1 of 2, found %i reports" % len(reports)) 1053 return -1 1054 return 0 1055 1056 def CloseOutputFile(self): 1057 self.out.close() 1058 1059 1060 class ThreadSanitizerRV1Mixin(object): 1061 """RaceVerifier first pass. 1062 1063 Runs ThreadSanitizer as usual, but hides race reports and collects them in a 1064 temporary file""" 1065 1066 def __init__(self): 1067 super(ThreadSanitizerRV1Mixin, self).__init__() 1068 self.RegisterOptionParserHook(ThreadSanitizerRV1Mixin.ExtendOptionParser) 1069 1070 def ExtendOptionParser(self, parser): 1071 parser.set_defaults(hybrid="yes") 1072 1073 def CreateAnalyzer(self): 1074 use_gdb = common.IsMac() 1075 self.analyzer = ThreadSanitizerRV1Analyzer(self._source_dir, use_gdb) 1076 return self.analyzer 1077 1078 def Cleanup(self): 1079 super(ThreadSanitizerRV1Mixin, self).Cleanup() 1080 self.analyzer.CloseOutputFile() 1081 1082 1083 class ThreadSanitizerRV2Mixin(object): 1084 """RaceVerifier second pass.""" 1085 1086 def __init__(self): 1087 super(ThreadSanitizerRV2Mixin, self).__init__() 1088 self.RegisterOptionParserHook(ThreadSanitizerRV2Mixin.ExtendOptionParser) 1089 1090 def ExtendOptionParser(self, parser): 1091 parser.add_option("", "--race-verifier-sleep-ms", 1092 dest="race_verifier_sleep_ms", default=10, 1093 help="duration of RaceVerifier delays") 1094 1095 def ToolSpecificFlags(self): 1096 proc = super(ThreadSanitizerRV2Mixin, self).ToolSpecificFlags() 1097 proc += ['--race-verifier=%s' % ThreadSanitizerRV1Analyzer.TMP_FILE, 1098 '--race-verifier-sleep-ms=%d' % 1099 int(self._options.race_verifier_sleep_ms)] 1100 return proc 1101 1102 def Cleanup(self): 1103 super(ThreadSanitizerRV2Mixin, self).Cleanup() 1104 os.unlink(ThreadSanitizerRV1Analyzer.TMP_FILE) 1105 1106 1107 class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin, ThreadSanitizerPosix): 1108 pass 1109 1110 1111 class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin, ThreadSanitizerPosix): 1112 pass 1113 1114 1115 class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin, 1116 ThreadSanitizerWindows): 1117 pass 1118 1119 1120 class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin, 1121 ThreadSanitizerWindows): 1122 pass 1123 1124 1125 class RaceVerifier(object): 1126 """Runs tests under RaceVerifier/Valgrind.""" 1127 1128 MORE_INFO_URL = "http://code.google.com/p/data-race-test/wiki/RaceVerifier" 1129 1130 def RV1Factory(self): 1131 if common.IsWindows(): 1132 return ThreadSanitizerRV1Windows() 1133 else: 1134 return ThreadSanitizerRV1Posix() 1135 1136 def RV2Factory(self): 1137 if common.IsWindows(): 1138 return ThreadSanitizerRV2Windows() 1139 else: 1140 return ThreadSanitizerRV2Posix() 1141 1142 def ToolName(self): 1143 return "tsan" 1144 1145 def Main(self, args, check_sanity, min_runtime_in_seconds): 1146 logging.info("Running a TSan + RaceVerifier test. For more information, " + 1147 "see " + self.MORE_INFO_URL) 1148 cmd1 = self.RV1Factory() 1149 ret = cmd1.Main(args, check_sanity, min_runtime_in_seconds) 1150 # Verify race reports, if there are any. 1151 if ret == -1: 1152 logging.info("Starting pass 2 of 2. Running the same binary in " + 1153 "RaceVerifier mode to confirm possible race reports.") 1154 logging.info("For more information, see " + self.MORE_INFO_URL) 1155 cmd2 = self.RV2Factory() 1156 ret = cmd2.Main(args, check_sanity, min_runtime_in_seconds) 1157 else: 1158 logging.info("No reports, skipping RaceVerifier second pass") 1159 logging.info("Please see " + self.MORE_INFO_URL + " for more information " + 1160 "on RaceVerifier") 1161 return ret 1162 1163 def Run(self, args, module, min_runtime_in_seconds=0): 1164 return self.Main(args, False, min_runtime_in_seconds) 1165 1166 1167 class EmbeddedTool(BaseTool): 1168 """Abstract class for tools embedded directly into the test binary. 1169 """ 1170 # TODO(glider): need to override Execute() and support process chaining here. 1171 1172 def ToolCommand(self): 1173 # In the simplest case just the args of the script. 1174 return self._args 1175 1176 1177 class Asan(EmbeddedTool): 1178 """AddressSanitizer, a memory error detector. 1179 1180 More information at 1181 http://dev.chromium.org/developers/testing/addresssanitizer 1182 """ 1183 def __init__(self): 1184 super(Asan, self).__init__() 1185 self._timeout = 1200 1186 if common.IsMac(): 1187 self._env["DYLD_NO_PIE"] = "1" 1188 1189 1190 def ToolName(self): 1191 return "asan" 1192 1193 def ToolCommand(self): 1194 # TODO(glider): use pipes instead of the ugly wrapper here once they 1195 # are supported. 1196 procs = [os.path.join(self._source_dir, "tools", "valgrind", 1197 "asan", "asan_wrapper.sh")] 1198 procs.extend(self._args) 1199 return procs 1200 1201 def Analyze(sels, unused_check_sanity): 1202 return 0 1203 1204 1205 class ToolFactory: 1206 def Create(self, tool_name): 1207 if tool_name == "memcheck": 1208 return Memcheck() 1209 if tool_name == "tsan": 1210 if common.IsWindows(): 1211 return ThreadSanitizerWindows() 1212 else: 1213 return ThreadSanitizerPosix() 1214 if tool_name == "drmemory" or tool_name == "drmemory_light": 1215 # TODO(timurrrr): remove support for "drmemory" when buildbots are 1216 # switched to drmemory_light OR make drmemory==drmemory_full the default 1217 # mode when the tool is mature enough. 1218 return DrMemory(False, False) 1219 if tool_name == "drmemory_full": 1220 return DrMemory(True, False) 1221 if tool_name == "drmemory_pattern": 1222 return DrMemory(False, True) 1223 if tool_name == "tsan_rv": 1224 return RaceVerifier() 1225 if tool_name == "asan": 1226 return Asan() 1227 try: 1228 platform_name = common.PlatformNames()[0] 1229 except common.NotImplementedError: 1230 platform_name = sys.platform + "(Unknown)" 1231 raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name, 1232 platform_name) 1233 1234 def CreateTool(tool): 1235 return ToolFactory().Create(tool) 1236