valgrind_test.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
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 6directory. 7""" 8 9import datetime 10import glob 11import logging 12import optparse 13import os 14import re 15import shutil 16import stat 17import subprocess 18import sys 19import tempfile 20 21import common 22 23import drmemory_analyze 24import memcheck_analyze 25import tsan_analyze 26 27class 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 240class 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 valgrind " 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 proc = ["valgrind", "--tool=%s" % tool_name] 361 362 proc += ["--num-callers=%i" % int(self._options.num_callers)] 363 364 if self._options.trace_children: 365 proc += ["--trace-children=yes"] 366 proc += ["--trace-children-skip='*dbus-daemon*'"] 367 proc += ["--trace-children-skip='*dbus-launch*'"] 368 proc += ["--trace-children-skip='*perl*'"] 369 proc += ["--trace-children-skip='*python*'"] 370 371 proc += self.ToolSpecificFlags() 372 proc += self._tool_flags 373 374 suppression_count = 0 375 for suppression_file in self._options.suppressions: 376 if os.path.exists(suppression_file): 377 suppression_count += 1 378 proc += ["--suppressions=%s" % suppression_file] 379 380 if not suppression_count: 381 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 382 383 logfilename = self.log_dir + ("/%s." % tool_name) + "%p" 384 if self.UseXML(): 385 proc += ["--xml=yes", "--xml-file=" + logfilename] 386 else: 387 proc += ["--log-file=" + logfilename] 388 389 # The Valgrind command is constructed. 390 391 # Handle --indirect_webkit_layout separately. 392 if self._options.indirect_webkit_layout: 393 # Need to create the wrapper before modifying |proc|. 394 wrapper = self.CreateBrowserWrapper(proc, webkit=True) 395 proc = self._args 396 proc.append("--wrapper") 397 proc.append(wrapper) 398 return proc 399 400 # Valgrind doesn't play nice with the Chrome sandbox. Empty this env var 401 # set by runtest.py to disable the sandbox. 402 if os.environ.get("CHROME_DEVEL_SANDBOX", None): 403 logging.info("Removing CHROME_DEVEL_SANDBOX fron environment") 404 os.environ["CHROME_DEVEL_SANDBOX"] = '' 405 406 if self._options.indirect: 407 wrapper = self.CreateBrowserWrapper(proc) 408 os.environ["BROWSER_WRAPPER"] = wrapper 409 logging.info('export BROWSER_WRAPPER=' + wrapper) 410 proc = [] 411 proc += self._args 412 return proc 413 414 def ToolSpecificFlags(self): 415 raise NotImplementedError, "This method should be implemented " \ 416 "in the tool-specific subclass" 417 418 def CreateBrowserWrapper(self, proc, webkit=False): 419 """The program being run invokes Python or something else that can't stand 420 to be valgrinded, and also invokes the Chrome browser. In this case, use a 421 magic wrapper to only valgrind the Chrome browser. Build the wrapper here. 422 Returns the path to the wrapper. It's up to the caller to use the wrapper 423 appropriately. 424 """ 425 command = " ".join(proc) 426 # Add the PID of the browser wrapper to the logfile names so we can 427 # separate log files for different UI tests at the analyze stage. 428 command = command.replace("%p", "$$.%p") 429 430 (fd, indirect_fname) = tempfile.mkstemp(dir=self.log_dir, 431 prefix="browser_wrapper.", 432 text=True) 433 f = os.fdopen(fd, "w") 434 f.write('#!/bin/bash\n' 435 'echo "Started Valgrind wrapper for this test, PID=$$" >&2\n') 436 437 f.write('DIR=`dirname $0`\n' 438 'TESTNAME_FILE=$DIR/testcase.$$.name\n\n') 439 440 if webkit: 441 # Webkit layout_tests pass the URL as the first line of stdin. 442 f.write('tee $TESTNAME_FILE | %s "$@"\n' % command) 443 else: 444 # Try to get the test case name by looking at the program arguments. 445 # i.e. Chromium ui_tests used --test-name arg. 446 # TODO(timurrrr): This doesn't handle "--test-name Test.Name" 447 # TODO(timurrrr): ui_tests are dead. Where do we use the non-webkit 448 # wrapper now? browser_tests? What do they do? 449 f.write('for arg in $@\ndo\n' 450 ' if [[ "$arg" =~ --test-name=(.*) ]]\n then\n' 451 ' echo ${BASH_REMATCH[1]} >$TESTNAME_FILE\n' 452 ' fi\n' 453 'done\n\n' 454 '%s "$@"\n' % command) 455 456 f.close() 457 os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR) 458 return indirect_fname 459 460 def CreateAnalyzer(self): 461 raise NotImplementedError, "This method should be implemented " \ 462 "in the tool-specific subclass" 463 464 def GetAnalyzeResults(self, check_sanity=False): 465 # Glob all the files in the log directory 466 filenames = glob.glob(self.log_dir + "/" + self.ToolName() + ".*") 467 468 # If we have browser wrapper, the logfiles are named as 469 # "toolname.wrapper_PID.valgrind_PID". 470 # Let's extract the list of wrapper_PIDs and name it ppids 471 ppids = set([int(f.split(".")[-2]) \ 472 for f in filenames if re.search("\.[0-9]+\.[0-9]+$", f)]) 473 474 analyzer = self.CreateAnalyzer() 475 if len(ppids) == 0: 476 # Fast path - no browser wrapper was set. 477 return analyzer.Report(filenames, None, check_sanity) 478 479 ret = 0 480 for ppid in ppids: 481 testcase_name = None 482 try: 483 f = open(self.log_dir + ("/testcase.%d.name" % ppid)) 484 testcase_name = f.read().strip() 485 f.close() 486 wk_layout_prefix="third_party/WebKit/LayoutTests/" 487 wk_prefix_at = testcase_name.rfind(wk_layout_prefix) 488 if wk_prefix_at != -1: 489 testcase_name = testcase_name[wk_prefix_at + len(wk_layout_prefix):] 490 except IOError: 491 pass 492 print "=====================================================" 493 print " Below is the report for valgrind wrapper PID=%d." % ppid 494 if testcase_name: 495 print " It was used while running the `%s` test." % testcase_name 496 else: 497 print " You can find the corresponding test" 498 print " by searching the above log for 'PID=%d'" % ppid 499 sys.stdout.flush() 500 501 ppid_filenames = [f for f in filenames \ 502 if re.search("\.%d\.[0-9]+$" % ppid, f)] 503 # check_sanity won't work with browser wrappers 504 assert check_sanity == False 505 ret |= analyzer.Report(ppid_filenames, testcase_name) 506 print "=====================================================" 507 sys.stdout.flush() 508 509 if ret != 0: 510 print "" 511 print "The Valgrind reports are grouped by test names." 512 print "Each test has its PID printed in the log when the test was run" 513 print "and at the beginning of its Valgrind report." 514 print "Hint: you can search for the reports by Ctrl+F -> `=#`" 515 sys.stdout.flush() 516 517 return ret 518 519 520# TODO(timurrrr): Split into a separate file. 521class Memcheck(ValgrindTool): 522 """Memcheck 523 Dynamic memory error detector for Linux & Mac 524 525 http://valgrind.org/info/tools.html#memcheck 526 """ 527 528 def __init__(self): 529 super(Memcheck, self).__init__() 530 self.RegisterOptionParserHook(Memcheck.ExtendOptionParser) 531 532 def ToolName(self): 533 return "memcheck" 534 535 def ExtendOptionParser(self, parser): 536 parser.add_option("--leak-check", "--leak_check", type="string", 537 default="yes", # --leak-check=yes is equivalent of =full 538 help="perform leak checking at the end of the run") 539 parser.add_option("", "--show_all_leaks", action="store_true", 540 default=False, 541 help="also show less blatant leaks") 542 parser.add_option("", "--track_origins", action="store_true", 543 default=False, 544 help="Show whence uninitialized bytes came. 30% slower.") 545 546 def ToolSpecificFlags(self): 547 ret = ["--gen-suppressions=all", "--demangle=no"] 548 ret += ["--leak-check=%s" % self._options.leak_check] 549 550 if self._options.show_all_leaks: 551 ret += ["--show-reachable=yes"] 552 else: 553 ret += ["--show-possibly-lost=no"] 554 555 if self._options.track_origins: 556 ret += ["--track-origins=yes"] 557 558 # TODO(glider): this is a temporary workaround for http://crbug.com/51716 559 # Let's see whether it helps. 560 if common.IsMac(): 561 ret += ["--smc-check=all"] 562 563 return ret 564 565 def CreateAnalyzer(self): 566 use_gdb = common.IsMac() 567 return memcheck_analyze.MemcheckAnalyzer(self._source_dir, 568 self._options.show_all_leaks, 569 use_gdb=use_gdb) 570 571 def Analyze(self, check_sanity=False): 572 ret = self.GetAnalyzeResults(check_sanity) 573 574 if ret != 0: 575 logging.info("Please see http://dev.chromium.org/developers/how-tos/" 576 "using-valgrind for the info on Memcheck/Valgrind") 577 return ret 578 579 580class PinTool(BaseTool): 581 """Abstract class for running PIN tools. 582 583 Always subclass this and implement ToolSpecificFlags() and 584 ExtendOptionParser() for tool-specific stuff. 585 """ 586 def PrepareForTest(self): 587 pass 588 589 def ToolSpecificFlags(self): 590 raise NotImplementedError, "This method should be implemented " \ 591 "in the tool-specific subclass" 592 593 def ToolCommand(self): 594 """Get the PIN command to run.""" 595 596 # Construct the PIN command. 597 pin_cmd = os.getenv("PIN_COMMAND") 598 if not pin_cmd: 599 raise RuntimeError, "Please set PIN_COMMAND environment variable " \ 600 "with the path to pin.exe" 601 proc = pin_cmd.split(" ") 602 603 proc += self.ToolSpecificFlags() 604 605 # The PIN command is constructed. 606 607 # PIN requires -- to separate PIN flags from the executable name. 608 # self._args begins with the exe to be run. 609 proc += ["--"] 610 611 proc += self._args 612 return proc 613 614 615class ThreadSanitizerBase(object): 616 """ThreadSanitizer 617 Dynamic data race detector for Linux, Mac and Windows. 618 619 http://code.google.com/p/data-race-test/wiki/ThreadSanitizer 620 621 Since TSan works on both Valgrind (Linux, Mac) and PIN (Windows), we need 622 to have multiple inheritance 623 """ 624 625 INFO_MESSAGE="Please see http://dev.chromium.org/developers/how-tos/" \ 626 "using-valgrind/threadsanitizer for the info on " \ 627 "ThreadSanitizer" 628 629 def __init__(self): 630 super(ThreadSanitizerBase, self).__init__() 631 self.RegisterOptionParserHook(ThreadSanitizerBase.ExtendOptionParser) 632 633 def ToolName(self): 634 return "tsan" 635 636 def UseXML(self): 637 return False 638 639 def SelfContained(self): 640 return True 641 642 def ExtendOptionParser(self, parser): 643 parser.add_option("", "--hybrid", default="no", 644 dest="hybrid", 645 help="Finds more data races, may give false positive " 646 "reports unless the code is annotated") 647 parser.add_option("", "--announce-threads", default="yes", 648 dest="announce_threads", 649 help="Show the the stack traces of thread creation") 650 parser.add_option("", "--free-is-write", default="no", 651 dest="free_is_write", 652 help="Treat free()/operator delete as memory write. " 653 "This helps finding more data races, but (currently) " 654 "this may give false positive reports on std::string " 655 "internals, see http://code.google.com/p/data-race-test" 656 "/issues/detail?id=40") 657 658 def EvalBoolFlag(self, flag_value): 659 if (flag_value in ["1", "true", "yes"]): 660 return True 661 elif (flag_value in ["0", "false", "no"]): 662 return False 663 raise RuntimeError, "Can't parse flag value (%s)" % flag_value 664 665 def ToolSpecificFlags(self): 666 ret = [] 667 668 ignore_files = ["ignores.txt"] 669 for platform_suffix in common.PlatformNames(): 670 ignore_files.append("ignores_%s.txt" % platform_suffix) 671 for ignore_file in ignore_files: 672 fullname = os.path.join(self._source_dir, 673 "tools", "valgrind", "tsan", ignore_file) 674 if os.path.exists(fullname): 675 fullname = common.NormalizeWindowsPath(fullname) 676 ret += ["--ignore=%s" % fullname] 677 678 # This should shorten filepaths for local builds. 679 ret += ["--file-prefix-to-cut=%s/" % self._source_dir] 680 681 # This should shorten filepaths on bots. 682 ret += ["--file-prefix-to-cut=build/src/"] 683 684 # This should shorten filepaths for functions intercepted in TSan. 685 ret += ["--file-prefix-to-cut=scripts/tsan/tsan/"] 686 687 ret += ["--gen-suppressions=true"] 688 689 if self.EvalBoolFlag(self._options.hybrid): 690 ret += ["--hybrid=yes"] # "no" is the default value for TSAN 691 692 if self.EvalBoolFlag(self._options.announce_threads): 693 ret += ["--announce-threads"] 694 695 if self.EvalBoolFlag(self._options.free_is_write): 696 ret += ["--free-is-write=yes"] 697 else: 698 ret += ["--free-is-write=no"] 699 700 701 # --show-pc flag is needed for parsing the error logs on Darwin. 702 if platform_suffix == 'mac': 703 ret += ["--show-pc=yes"] 704 ret += ["--show-pid=no"] 705 706 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False) 707 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .." 708 for bc in boring_callers: 709 ret += ["--cut_stack_below=%s" % bc] 710 711 return ret 712 713 714class ThreadSanitizerPosix(ThreadSanitizerBase, ValgrindTool): 715 def ToolSpecificFlags(self): 716 proc = ThreadSanitizerBase.ToolSpecificFlags(self) 717 # The -v flag is needed for printing the list of used suppressions and 718 # obtaining addresses for loaded shared libraries on Mac. 719 proc += ["-v"] 720 return proc 721 722 def CreateAnalyzer(self): 723 use_gdb = common.IsMac() 724 return tsan_analyze.TsanAnalyzer(self._source_dir, use_gdb) 725 726 def Analyze(self, check_sanity=False): 727 ret = self.GetAnalyzeResults(check_sanity) 728 729 if ret != 0: 730 logging.info(self.INFO_MESSAGE) 731 return ret 732 733 734class ThreadSanitizerWindows(ThreadSanitizerBase, PinTool): 735 736 def __init__(self): 737 super(ThreadSanitizerWindows, self).__init__() 738 self.RegisterOptionParserHook(ThreadSanitizerWindows.ExtendOptionParser) 739 740 def ExtendOptionParser(self, parser): 741 parser.add_option("", "--suppressions", default=[], 742 action="append", 743 help="path to TSan suppression file") 744 745 746 def ToolSpecificFlags(self): 747 proc = ThreadSanitizerBase.ToolSpecificFlags(self) 748 # On PIN, ThreadSanitizer has its own suppression mechanism 749 # and --log-file flag which work exactly on Valgrind. 750 suppression_count = 0 751 for suppression_file in self._options.suppressions: 752 if os.path.exists(suppression_file): 753 suppression_count += 1 754 suppression_file = common.NormalizeWindowsPath(suppression_file) 755 proc += ["--suppressions=%s" % suppression_file] 756 757 if not suppression_count: 758 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 759 760 logfilename = self.log_dir + "/tsan.%p" 761 proc += ["--log-file=" + common.NormalizeWindowsPath(logfilename)] 762 763 # TODO(timurrrr): Add flags for Valgrind trace children analog when we 764 # start running complex tests (e.g. UI) under TSan/Win. 765 766 return proc 767 768 def Analyze(self, check_sanity=False): 769 filenames = glob.glob(self.log_dir + "/tsan.*") 770 analyzer = tsan_analyze.TsanAnalyzer(self._source_dir) 771 ret = analyzer.Report(filenames, None, check_sanity) 772 if ret != 0: 773 logging.info(self.INFO_MESSAGE) 774 return ret 775 776 777class DrMemory(BaseTool): 778 """Dr.Memory 779 Dynamic memory error detector for Windows. 780 781 http://dev.chromium.org/developers/how-tos/using-drmemory 782 It is not very mature at the moment, some things might not work properly. 783 """ 784 785 def __init__(self, full_mode, pattern_mode): 786 super(DrMemory, self).__init__() 787 self.full_mode = full_mode 788 self.pattern_mode = pattern_mode 789 self.RegisterOptionParserHook(DrMemory.ExtendOptionParser) 790 791 def ToolName(self): 792 return "drmemory" 793 794 def ExtendOptionParser(self, parser): 795 parser.add_option("", "--suppressions", default=[], 796 action="append", 797 help="path to a drmemory suppression file") 798 parser.add_option("", "--follow_python", action="store_true", 799 default=False, dest="follow_python", 800 help="Monitor python child processes. If off, neither " 801 "python children nor any children of python children " 802 "will be monitored.") 803 parser.add_option("", "--indirect", action="store_true", 804 default=False, 805 help="set BROWSER_WRAPPER rather than " 806 "running Dr. Memory directly on the harness") 807 parser.add_option("", "--use_debug", action="store_true", 808 default=False, dest="use_debug", 809 help="Run Dr. Memory debug build") 810 parser.add_option("", "--trace_children", action="store_true", 811 default=True, 812 help="TODO: default value differs from Valgrind") 813 814 def ToolCommand(self): 815 """Get the tool command to run.""" 816 # WINHEAP is what Dr. Memory supports as there are issues w/ both 817 # jemalloc (http://code.google.com/p/drmemory/issues/detail?id=320) and 818 # tcmalloc (http://code.google.com/p/drmemory/issues/detail?id=314) 819 add_env = { 820 "CHROME_ALLOCATOR" : "WINHEAP", 821 "JSIMD_FORCEMMX" : "1", # http://code.google.com/p/drmemory/issues/detail?id=540 822 } 823 for k,v in add_env.iteritems(): 824 logging.info("export %s=%s", k, v) 825 os.putenv(k, v) 826 827 drmem_cmd = os.getenv("DRMEMORY_COMMAND") 828 if not drmem_cmd: 829 raise RuntimeError, "Please set DRMEMORY_COMMAND environment variable " \ 830 "with the path to drmemory.exe" 831 proc = drmem_cmd.split(" ") 832 833 # By default, don't run python (this will exclude python's children as well) 834 # to reduce runtime. We're not really interested in spending time finding 835 # bugs in the python implementation. 836 # With file-based config we must update the file every time, and 837 # it will affect simultaneous drmem uses by this user. While file-based 838 # config has many advantages, here we may want this-instance-only 839 # (http://code.google.com/p/drmemory/issues/detail?id=334). 840 drconfig_cmd = [ proc[0].replace("drmemory.exe", "drconfig.exe") ] 841 drconfig_cmd += ["-quiet"] # suppress errors about no 64-bit libs 842 run_drconfig = True 843 if self._options.follow_python: 844 logging.info("Following python children") 845 # -unreg fails if not already registered so query for that first 846 query_cmd = drconfig_cmd + ["-isreg", "python.exe"] 847 query_proc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE, 848 shell=True) 849 (query_out, query_err) = query_proc.communicate() 850 if re.search("exe not registered", query_out): 851 run_drconfig = False # all set 852 else: 853 drconfig_cmd += ["-unreg", "python.exe"] 854 else: 855 logging.info("Excluding python children") 856 drconfig_cmd += ["-reg", "python.exe", "-norun"] 857 if run_drconfig: 858 drconfig_retcode = common.RunSubprocess(drconfig_cmd, self._timeout) 859 if drconfig_retcode: 860 logging.error("Configuring whether to follow python children failed " \ 861 "with %d.", drconfig_retcode) 862 raise RuntimeError, "Configuring python children failed " 863 864 suppression_count = 0 865 supp_files = self._options.suppressions 866 if self.full_mode: 867 supp_files += [s.replace(".txt", "_full.txt") for s in supp_files] 868 for suppression_file in supp_files: 869 if os.path.exists(suppression_file): 870 suppression_count += 1 871 proc += ["-suppress", common.NormalizeWindowsPath(suppression_file)] 872 873 if not suppression_count: 874 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 875 876 # Un-comment to dump Dr.Memory events on error 877 #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"] 878 879 # Un-comment and comment next line to debug Dr.Memory 880 #proc += ["-dr_ops", "-no_hide"] 881 #proc += ["-dr_ops", "-msgbox_mask", "-dr_ops", "15"] 882 #Proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "15"] 883 # Ensure we see messages about Dr. Memory crashing! 884 proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "12"] 885 886 if self._options.use_debug: 887 proc += ["-debug"] 888 889 proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)] 890 891 if self.log_parent_dir: 892 # gpu process on Windows Vista+ runs at Low Integrity and can only 893 # write to certain directories (http://crbug.com/119131) 894 symcache_dir = os.path.join(self.log_parent_dir, "drmemory.symcache") 895 elif self._options.build_dir: 896 # The other case is only possible with -t cmdline. 897 # Anyways, if we omit -symcache_dir the -logdir's value is used which 898 # should be fine. 899 symcache_dir = os.path.join(self._options.build_dir, "drmemory.symcache") 900 if symcache_dir: 901 if not os.path.exists(symcache_dir): 902 try: 903 os.mkdir(symcache_dir) 904 except OSError: 905 logging.warning("Can't create symcache dir?") 906 if os.path.exists(symcache_dir): 907 proc += ["-symcache_dir", common.NormalizeWindowsPath(symcache_dir)] 908 909 # Use -no_summary to suppress DrMemory's summary and init-time 910 # notifications. We generate our own with drmemory_analyze.py. 911 proc += ["-batch", "-no_summary"] 912 913 # Un-comment to disable interleaved output. Will also suppress error 914 # messages normally printed to stderr. 915 #proc += ["-quiet", "-no_results_to_stderr"] 916 917 proc += ["-callstack_max_frames", "40"] 918 919 # make callstacks easier to read 920 proc += ["-callstack_srcfile_prefix", 921 "build\\src,chromium\\src,crt_build\\self_x86"] 922 proc += ["-callstack_modname_hide", 923 "*drmemory*,chrome.dll"] 924 925 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False) 926 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .." 927 proc += ["-callstack_truncate_below", ",".join(boring_callers)] 928 929 if self.pattern_mode: 930 proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"] 931 elif not self.full_mode: 932 proc += ["-light"] 933 934 proc += self._tool_flags 935 936 # DrM i#850/851: The new -callstack_use_top_fp_selectively has bugs. 937 proc += ["-no_callstack_use_top_fp_selectively"] 938 939 # Dr.Memory requires -- to separate tool flags from the executable name. 940 proc += ["--"] 941 942 if self._options.indirect: 943 # TODO(timurrrr): reuse for TSan on Windows 944 wrapper_path = os.path.join(self._source_dir, 945 "tools", "valgrind", "browser_wrapper_win.py") 946 self.CreateBrowserWrapper(" ".join(["python", wrapper_path] + proc)) 947 logging.info("browser wrapper = " + " ".join(proc)) 948 proc = [] 949 950 # Note that self._args begins with the name of the exe to be run. 951 self._args[0] = common.NormalizeWindowsPath(self._args[0]) 952 proc += self._args 953 return proc 954 955 def CreateBrowserWrapper(self, command): 956 os.putenv("BROWSER_WRAPPER", command) 957 958 def Analyze(self, check_sanity=False): 959 # Use one analyzer for all the log files to avoid printing duplicate reports 960 # 961 # TODO(timurrrr): unify this with Valgrind and other tools when we have 962 # http://code.google.com/p/drmemory/issues/detail?id=684 963 analyzer = drmemory_analyze.DrMemoryAnalyzer() 964 965 ret = 0 966 if not self._options.indirect: 967 filenames = glob.glob(self.log_dir + "/*/results.txt") 968 969 ret = analyzer.Report(filenames, None, check_sanity) 970 else: 971 testcases = glob.glob(self.log_dir + "/testcase.*.logs") 972 # If we have browser wrapper, the per-test logdirs are named as 973 # "testcase.wrapper_PID.name". 974 # Let's extract the list of wrapper_PIDs and name it ppids. 975 # NOTE: ppids may contain '_', i.e. they are not ints! 976 ppids = set([f.split(".")[-2] for f in testcases]) 977 978 for ppid in ppids: 979 testcase_name = None 980 try: 981 f = open("%s/testcase.%s.name" % (self.log_dir, ppid)) 982 testcase_name = f.read().strip() 983 f.close() 984 except IOError: 985 pass 986 print "=====================================================" 987 print " Below is the report for drmemory wrapper PID=%s." % ppid 988 if testcase_name: 989 print " It was used while running the `%s` test." % testcase_name 990 else: 991 # TODO(timurrrr): hm, the PID line is suppressed on Windows... 992 print " You can find the corresponding test" 993 print " by searching the above log for 'PID=%s'" % ppid 994 sys.stdout.flush() 995 ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" % 996 (self.log_dir, ppid)) 997 ret |= analyzer.Report(ppid_filenames, testcase_name, False) 998 print "=====================================================" 999 sys.stdout.flush() 1000 1001 logging.info("Please see http://dev.chromium.org/developers/how-tos/" 1002 "using-drmemory for the info on Dr. Memory") 1003 return ret 1004 1005 1006# RaceVerifier support. See 1007# http://code.google.com/p/data-race-test/wiki/RaceVerifier for more details. 1008class ThreadSanitizerRV1Analyzer(tsan_analyze.TsanAnalyzer): 1009 """ TsanAnalyzer that saves race reports to a file. """ 1010 1011 TMP_FILE = "rvlog.tmp" 1012 1013 def __init__(self, source_dir, use_gdb): 1014 super(ThreadSanitizerRV1Analyzer, self).__init__(source_dir, use_gdb) 1015 self.out = open(self.TMP_FILE, "w") 1016 1017 def Report(self, files, testcase, check_sanity=False): 1018 reports = self.GetReports(files) 1019 for report in reports: 1020 print >>self.out, report 1021 if len(reports) > 0: 1022 logging.info("RaceVerifier pass 1 of 2, found %i reports" % len(reports)) 1023 return -1 1024 return 0 1025 1026 def CloseOutputFile(self): 1027 self.out.close() 1028 1029 1030class ThreadSanitizerRV1Mixin(object): 1031 """RaceVerifier first pass. 1032 1033 Runs ThreadSanitizer as usual, but hides race reports and collects them in a 1034 temporary file""" 1035 1036 def __init__(self): 1037 super(ThreadSanitizerRV1Mixin, self).__init__() 1038 self.RegisterOptionParserHook(ThreadSanitizerRV1Mixin.ExtendOptionParser) 1039 1040 def ExtendOptionParser(self, parser): 1041 parser.set_defaults(hybrid="yes") 1042 1043 def CreateAnalyzer(self): 1044 use_gdb = common.IsMac() 1045 self.analyzer = ThreadSanitizerRV1Analyzer(self._source_dir, use_gdb) 1046 return self.analyzer 1047 1048 def Cleanup(self): 1049 super(ThreadSanitizerRV1Mixin, self).Cleanup() 1050 self.analyzer.CloseOutputFile() 1051 1052 1053class ThreadSanitizerRV2Mixin(object): 1054 """RaceVerifier second pass.""" 1055 1056 def __init__(self): 1057 super(ThreadSanitizerRV2Mixin, self).__init__() 1058 self.RegisterOptionParserHook(ThreadSanitizerRV2Mixin.ExtendOptionParser) 1059 1060 def ExtendOptionParser(self, parser): 1061 parser.add_option("", "--race-verifier-sleep-ms", 1062 dest="race_verifier_sleep_ms", default=10, 1063 help="duration of RaceVerifier delays") 1064 1065 def ToolSpecificFlags(self): 1066 proc = super(ThreadSanitizerRV2Mixin, self).ToolSpecificFlags() 1067 proc += ['--race-verifier=%s' % ThreadSanitizerRV1Analyzer.TMP_FILE, 1068 '--race-verifier-sleep-ms=%d' % 1069 int(self._options.race_verifier_sleep_ms)] 1070 return proc 1071 1072 def Cleanup(self): 1073 super(ThreadSanitizerRV2Mixin, self).Cleanup() 1074 os.unlink(ThreadSanitizerRV1Analyzer.TMP_FILE) 1075 1076 1077class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin, ThreadSanitizerPosix): 1078 pass 1079 1080 1081class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin, ThreadSanitizerPosix): 1082 pass 1083 1084 1085class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin, 1086 ThreadSanitizerWindows): 1087 pass 1088 1089 1090class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin, 1091 ThreadSanitizerWindows): 1092 pass 1093 1094 1095class RaceVerifier(object): 1096 """Runs tests under RaceVerifier/Valgrind.""" 1097 1098 MORE_INFO_URL = "http://code.google.com/p/data-race-test/wiki/RaceVerifier" 1099 1100 def RV1Factory(self): 1101 if common.IsWindows(): 1102 return ThreadSanitizerRV1Windows() 1103 else: 1104 return ThreadSanitizerRV1Posix() 1105 1106 def RV2Factory(self): 1107 if common.IsWindows(): 1108 return ThreadSanitizerRV2Windows() 1109 else: 1110 return ThreadSanitizerRV2Posix() 1111 1112 def ToolName(self): 1113 return "tsan" 1114 1115 def Main(self, args, check_sanity, min_runtime_in_seconds): 1116 logging.info("Running a TSan + RaceVerifier test. For more information, " + 1117 "see " + self.MORE_INFO_URL) 1118 cmd1 = self.RV1Factory() 1119 ret = cmd1.Main(args, check_sanity, min_runtime_in_seconds) 1120 # Verify race reports, if there are any. 1121 if ret == -1: 1122 logging.info("Starting pass 2 of 2. Running the same binary in " + 1123 "RaceVerifier mode to confirm possible race reports.") 1124 logging.info("For more information, see " + self.MORE_INFO_URL) 1125 cmd2 = self.RV2Factory() 1126 ret = cmd2.Main(args, check_sanity, min_runtime_in_seconds) 1127 else: 1128 logging.info("No reports, skipping RaceVerifier second pass") 1129 logging.info("Please see " + self.MORE_INFO_URL + " for more information " + 1130 "on RaceVerifier") 1131 return ret 1132 1133 def Run(self, args, module, min_runtime_in_seconds=0): 1134 return self.Main(args, False, min_runtime_in_seconds) 1135 1136 1137class EmbeddedTool(BaseTool): 1138 """Abstract class for tools embedded directly into the test binary. 1139 """ 1140 # TODO(glider): need to override Execute() and support process chaining here. 1141 1142 def ToolCommand(self): 1143 # In the simplest case just the args of the script. 1144 return self._args 1145 1146 1147class Asan(EmbeddedTool): 1148 """AddressSanitizer, a memory error detector. 1149 1150 More information at 1151 http://dev.chromium.org/developers/testing/addresssanitizer 1152 """ 1153 def __init__(self): 1154 super(Asan, self).__init__() 1155 self._timeout = 1200 1156 if common.IsMac(): 1157 self._env["DYLD_NO_PIE"] = "1" 1158 1159 1160 def ToolName(self): 1161 return "asan" 1162 1163 def ToolCommand(self): 1164 # TODO(glider): use pipes instead of the ugly wrapper here once they 1165 # are supported. 1166 procs = [os.path.join(self._source_dir, "tools", "valgrind", 1167 "asan", "asan_wrapper.sh")] 1168 procs.extend(self._args) 1169 return procs 1170 1171 def Analyze(sels, unused_check_sanity): 1172 return 0 1173 1174 1175class TsanGcc(EmbeddedTool): 1176 """ThreadSanitizer with compile-time instrumentation done using GCC. 1177 1178 More information at 1179 code.google.com/p/data-race-test/wiki/GccInstrumentation 1180 """ 1181 def __init__(self): 1182 super(TsanGcc, self).__init__() 1183 self.RegisterOptionParserHook(TsanGcc.ExtendOptionParser) 1184 1185 def ExtendOptionParser(self, parser): 1186 parser.add_option("", "--suppressions", default=[], 1187 action="append", 1188 help="path to TSan suppression file") 1189 1190 def Setup(self, args): 1191 if not super(TsanGcc, self).Setup(args): 1192 return False 1193 ld_library_paths = [] 1194 for tail in "lib32", "lib64": 1195 ld_library_paths.append( 1196 os.path.join(self._source_dir, "third_party", 1197 "compiler-tsan", "gcc-current", tail)) 1198 # LD_LIBRARY_PATH will be overriden. 1199 self._env["LD_LIBRARY_PATH"] = ":".join(ld_library_paths) 1200 1201 # TODO(glider): this is a temporary solution until Analyze is implemented. 1202 env_options = ["--error-exitcode=1"] 1203 # TODO(glider): merge this with other TSan suppressions code. 1204 suppression_count = 0 1205 for suppression_file in self._options.suppressions: 1206 if os.path.exists(suppression_file): 1207 suppression_count += 1 1208 env_options += ["--suppressions=%s" % suppression_file] 1209 if not suppression_count: 1210 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 1211 1212 self._env["TSAN_ARGS"] = " ".join(env_options) 1213 return True 1214 1215 def ToolName(self): 1216 return "tsan" 1217 1218 def Analyze(self, unused_check_sanity): 1219 # TODO(glider): this should use tsan_analyze.TsanAnalyzer. As a temporary 1220 # solution we set the exit code to 1 when a report occurs, because TSan-GCC 1221 # does not support the --log-file flag yet. 1222 return 0 1223 1224 1225class ToolFactory: 1226 def Create(self, tool_name): 1227 if tool_name == "memcheck": 1228 return Memcheck() 1229 if tool_name == "tsan": 1230 if common.IsWindows(): 1231 return ThreadSanitizerWindows() 1232 else: 1233 return ThreadSanitizerPosix() 1234 if tool_name == "drmemory" or tool_name == "drmemory_light": 1235 # TODO(timurrrr): remove support for "drmemory" when buildbots are 1236 # switched to drmemory_light OR make drmemory==drmemory_full the default 1237 # mode when the tool is mature enough. 1238 return DrMemory(False, False) 1239 if tool_name == "drmemory_full": 1240 return DrMemory(True, False) 1241 if tool_name == "drmemory_pattern": 1242 return DrMemory(False, True) 1243 if tool_name == "tsan_rv": 1244 return RaceVerifier() 1245 if tool_name == "tsan_gcc": 1246 return TsanGcc() 1247 if tool_name == "asan": 1248 return Asan() 1249 try: 1250 platform_name = common.PlatformNames()[0] 1251 except common.NotImplementedError: 1252 platform_name = sys.platform + "(Unknown)" 1253 raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name, 1254 platform_name) 1255 1256def CreateTool(tool): 1257 return ToolFactory().Create(tool) 1258