Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / valgrind / valgrind_test.py
blob9f132c8414a7272f423a7cb62b742ac13a7dd60d
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Runs an exe through Valgrind and puts the intermediate files in a
6 directory.
7 """
9 import datetime
10 import glob
11 import logging
12 import optparse
13 import os
14 import re
15 import shutil
16 import stat
17 import subprocess
18 import sys
19 import tempfile
21 import common
23 import drmemory_analyze
24 import memcheck_analyze
26 class BaseTool(object):
27 """Abstract class for running dynamic error detection tools.
29 Always subclass this and implement ToolCommand with framework- and
30 tool-specific stuff.
31 """
33 def __init__(self):
34 temp_parent_dir = None
35 self.log_parent_dir = ""
36 if common.IsWindows():
37 # gpu process on Windows Vista+ runs at Low Integrity and can only
38 # write to certain directories (http://crbug.com/119131)
40 # TODO(bruening): if scripts die in middle and don't clean up temp
41 # dir, we'll accumulate files in profile dir. should remove
42 # really old files automatically.
43 profile = os.getenv("USERPROFILE")
44 if profile:
45 self.log_parent_dir = profile + "\\AppData\\LocalLow\\"
46 if os.path.exists(self.log_parent_dir):
47 self.log_parent_dir = common.NormalizeWindowsPath(self.log_parent_dir)
48 temp_parent_dir = self.log_parent_dir
49 # Generated every time (even when overridden)
50 self.temp_dir = tempfile.mkdtemp(prefix="vg_logs_", dir=temp_parent_dir)
51 self.log_dir = self.temp_dir # overridable by --keep_logs
52 self.option_parser_hooks = []
53 # TODO(glider): we may not need some of the env vars on some of the
54 # platforms.
55 self._env = {
56 "G_SLICE" : "always-malloc",
57 "NSS_DISABLE_UNLOAD" : "1",
58 "NSS_DISABLE_ARENA_FREE_LIST" : "1",
59 "GTEST_DEATH_TEST_USE_FORK": "1",
62 def ToolName(self):
63 raise NotImplementedError, "This method should be implemented " \
64 "in the tool-specific subclass"
66 def Analyze(self, check_sanity=False):
67 raise NotImplementedError, "This method should be implemented " \
68 "in the tool-specific subclass"
70 def RegisterOptionParserHook(self, hook):
71 # Frameworks and tools can add their own flags to the parser.
72 self.option_parser_hooks.append(hook)
74 def CreateOptionParser(self):
75 # Defines Chromium-specific flags.
76 self._parser = optparse.OptionParser("usage: %prog [options] <program to "
77 "test>")
78 self._parser.disable_interspersed_args()
79 self._parser.add_option("-t", "--timeout",
80 dest="timeout", metavar="TIMEOUT", default=10000,
81 help="timeout in seconds for the run (default 10000)")
82 self._parser.add_option("", "--build-dir",
83 help="the location of the compiler output")
84 self._parser.add_option("", "--source-dir",
85 help="path to top of source tree for this build"
86 "(used to normalize source paths in baseline)")
87 self._parser.add_option("", "--gtest_filter", default="",
88 help="which test case to run")
89 self._parser.add_option("", "--gtest_repeat",
90 help="how many times to run each test")
91 self._parser.add_option("", "--gtest_print_time", action="store_true",
92 default=False,
93 help="show how long each test takes")
94 self._parser.add_option("", "--ignore_exit_code", action="store_true",
95 default=False,
96 help="ignore exit code of the test "
97 "(e.g. test failures)")
98 self._parser.add_option("", "--keep_logs", action="store_true",
99 default=False,
100 help="store memory tool logs in the <tool>.logs "
101 "directory instead of /tmp.\nThis can be "
102 "useful for tool developers/maintainers.\n"
103 "Please note that the <tool>.logs directory "
104 "will be clobbered on tool startup.")
106 # To add framework- or tool-specific flags, please add a hook using
107 # RegisterOptionParserHook in the corresponding subclass.
108 # See ValgrindTool for an example.
109 for hook in self.option_parser_hooks:
110 hook(self, self._parser)
112 def ParseArgv(self, args):
113 self.CreateOptionParser()
115 # self._tool_flags will store those tool flags which we don't parse
116 # manually in this script.
117 self._tool_flags = []
118 known_args = []
120 """ We assume that the first argument not starting with "-" is a program
121 name and all the following flags should be passed to the program.
122 TODO(timurrrr): customize optparse instead
124 while len(args) > 0 and args[0][:1] == "-":
125 arg = args[0]
126 if (arg == "--"):
127 break
128 if self._parser.has_option(arg.split("=")[0]):
129 known_args += [arg]
130 else:
131 self._tool_flags += [arg]
132 args = args[1:]
134 if len(args) > 0:
135 known_args += args
137 self._options, self._args = self._parser.parse_args(known_args)
139 self._timeout = int(self._options.timeout)
140 self._source_dir = self._options.source_dir
141 if self._options.keep_logs:
142 # log_parent_dir has trailing slash if non-empty
143 self.log_dir = self.log_parent_dir + "%s.logs" % self.ToolName()
144 if os.path.exists(self.log_dir):
145 shutil.rmtree(self.log_dir)
146 os.mkdir(self.log_dir)
147 logging.info("Logs are in " + self.log_dir)
149 self._ignore_exit_code = self._options.ignore_exit_code
150 if self._options.gtest_filter != "":
151 self._args.append("--gtest_filter=%s" % self._options.gtest_filter)
152 if self._options.gtest_repeat:
153 self._args.append("--gtest_repeat=%s" % self._options.gtest_repeat)
154 if self._options.gtest_print_time:
155 self._args.append("--gtest_print_time")
157 return True
159 def Setup(self, args):
160 return self.ParseArgv(args)
162 def ToolCommand(self):
163 raise NotImplementedError, "This method should be implemented " \
164 "in the tool-specific subclass"
166 def Cleanup(self):
167 # You may override it in the tool-specific subclass
168 pass
170 def Execute(self):
171 """ Execute the app to be tested after successful instrumentation.
172 Full execution command-line provided by subclassers via proc."""
173 logging.info("starting execution...")
174 proc = self.ToolCommand()
175 for var in self._env:
176 common.PutEnvAndLog(var, self._env[var])
177 return common.RunSubprocess(proc, self._timeout)
179 def RunTestsAndAnalyze(self, check_sanity):
180 exec_retcode = self.Execute()
181 analyze_retcode = self.Analyze(check_sanity)
183 if analyze_retcode:
184 logging.error("Analyze failed.")
185 logging.info("Search the log for '[ERROR]' to see the error reports.")
186 return analyze_retcode
188 if exec_retcode:
189 if self._ignore_exit_code:
190 logging.info("Test execution failed, but the exit code is ignored.")
191 else:
192 logging.error("Test execution failed.")
193 return exec_retcode
194 else:
195 logging.info("Test execution completed successfully.")
197 if not analyze_retcode:
198 logging.info("Analysis completed successfully.")
200 return 0
202 def Main(self, args, check_sanity, min_runtime_in_seconds):
203 """Call this to run through the whole process: Setup, Execute, Analyze"""
204 start_time = datetime.datetime.now()
205 retcode = -1
206 if self.Setup(args):
207 retcode = self.RunTestsAndAnalyze(check_sanity)
208 shutil.rmtree(self.temp_dir, ignore_errors=True)
209 self.Cleanup()
210 else:
211 logging.error("Setup failed")
212 end_time = datetime.datetime.now()
213 runtime_in_seconds = (end_time - start_time).seconds
214 hours = runtime_in_seconds / 3600
215 seconds = runtime_in_seconds % 3600
216 minutes = seconds / 60
217 seconds = seconds % 60
218 logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds))
219 if (min_runtime_in_seconds > 0 and
220 runtime_in_seconds < min_runtime_in_seconds):
221 logging.error("Layout tests finished too quickly. "
222 "It should have taken at least %d seconds. "
223 "Something went wrong?" % min_runtime_in_seconds)
224 retcode = -1
225 return retcode
227 def Run(self, args, module, min_runtime_in_seconds=0):
228 MODULES_TO_SANITY_CHECK = ["base"]
230 check_sanity = module in MODULES_TO_SANITY_CHECK
231 return self.Main(args, check_sanity, min_runtime_in_seconds)
234 class ValgrindTool(BaseTool):
235 """Abstract class for running Valgrind tools.
237 Always subclass this and implement ToolSpecificFlags() and
238 ExtendOptionParser() for tool-specific stuff.
240 def __init__(self):
241 super(ValgrindTool, self).__init__()
242 self.RegisterOptionParserHook(ValgrindTool.ExtendOptionParser)
244 def UseXML(self):
245 # Override if tool prefers nonxml output
246 return True
248 def ExtendOptionParser(self, parser):
249 parser.add_option("", "--suppressions", default=[],
250 action="append",
251 help="path to a valgrind suppression file")
252 parser.add_option("", "--indirect", action="store_true",
253 default=False,
254 help="set BROWSER_WRAPPER rather than "
255 "running valgrind directly")
256 parser.add_option("", "--indirect_webkit_layout", action="store_true",
257 default=False,
258 help="set --wrapper rather than running Dr. Memory "
259 "directly.")
260 parser.add_option("", "--trace_children", action="store_true",
261 default=False,
262 help="also trace child processes")
263 parser.add_option("", "--num-callers",
264 dest="num_callers", default=30,
265 help="number of callers to show in stack traces")
266 parser.add_option("", "--generate_dsym", action="store_true",
267 default=False,
268 help="Generate .dSYM file on Mac if needed. Slow!")
270 def Setup(self, args):
271 if not BaseTool.Setup(self, args):
272 return False
273 if common.IsMac():
274 self.PrepareForTestMac()
275 return True
277 def PrepareForTestMac(self):
278 """Runs dsymutil if needed.
280 Valgrind for Mac OS X requires that debugging information be in a .dSYM
281 bundle generated by dsymutil. It is not currently able to chase DWARF
282 data into .o files like gdb does, so executables without .dSYM bundles or
283 with the Chromium-specific "fake_dsym" bundles generated by
284 build/mac/strip_save_dsym won't give source file and line number
285 information in valgrind.
287 This function will run dsymutil if the .dSYM bundle is missing or if
288 it looks like a fake_dsym. A non-fake dsym that already exists is assumed
289 to be up-to-date.
291 test_command = self._args[0]
292 dsym_bundle = self._args[0] + '.dSYM'
293 dsym_file = os.path.join(dsym_bundle, 'Contents', 'Resources', 'DWARF',
294 os.path.basename(test_command))
295 dsym_info_plist = os.path.join(dsym_bundle, 'Contents', 'Info.plist')
297 needs_dsymutil = True
298 saved_test_command = None
300 if os.path.exists(dsym_file) and os.path.exists(dsym_info_plist):
301 # Look for the special fake_dsym tag in dsym_info_plist.
302 dsym_info_plist_contents = open(dsym_info_plist).read()
304 if not re.search('^\s*<key>fake_dsym</key>$', dsym_info_plist_contents,
305 re.MULTILINE):
306 # fake_dsym is not set, this is a real .dSYM bundle produced by
307 # dsymutil. dsymutil does not need to be run again.
308 needs_dsymutil = False
309 else:
310 # fake_dsym is set. dsym_file is a copy of the original test_command
311 # before it was stripped. Copy it back to test_command so that
312 # dsymutil has unstripped input to work with. Move the stripped
313 # test_command out of the way, it will be restored when this is
314 # done.
315 saved_test_command = test_command + '.stripped'
316 os.rename(test_command, saved_test_command)
317 shutil.copyfile(dsym_file, test_command)
318 shutil.copymode(saved_test_command, test_command)
320 if needs_dsymutil:
321 if self._options.generate_dsym:
322 # Remove the .dSYM bundle if it exists.
323 shutil.rmtree(dsym_bundle, True)
325 dsymutil_command = ['dsymutil', test_command]
327 # dsymutil is crazy slow. Ideally we'd have a timeout here,
328 # but common.RunSubprocess' timeout is only checked
329 # after each line of output; dsymutil is silent
330 # until the end, and is then killed, which is silly.
331 common.RunSubprocess(dsymutil_command)
333 if saved_test_command:
334 os.rename(saved_test_command, test_command)
335 else:
336 logging.info("No real .dSYM for test_command. Line numbers will "
337 "not be shown. Either tell xcode to generate .dSYM "
338 "file, or use --generate_dsym option to this tool.")
340 def ToolCommand(self):
341 """Get the valgrind command to run."""
342 # Note that self._args begins with the exe to be run.
343 tool_name = self.ToolName()
345 # Construct the valgrind command.
346 if 'CHROME_VALGRIND' in os.environ:
347 path = os.path.join(os.environ['CHROME_VALGRIND'], "bin", "valgrind")
348 else:
349 path = "valgrind"
350 proc = [path, "--tool=%s" % tool_name]
352 proc += ["--num-callers=%i" % int(self._options.num_callers)]
354 if self._options.trace_children:
355 proc += ["--trace-children=yes"]
356 proc += ["--trace-children-skip='*dbus-daemon*'"]
357 proc += ["--trace-children-skip='*dbus-launch*'"]
358 proc += ["--trace-children-skip='*perl*'"]
359 proc += ["--trace-children-skip='*python*'"]
360 # This is really Python, but for some reason Valgrind follows it.
361 proc += ["--trace-children-skip='*lsb_release*'"]
363 proc += self.ToolSpecificFlags()
364 proc += self._tool_flags
366 suppression_count = 0
367 for suppression_file in self._options.suppressions:
368 if os.path.exists(suppression_file):
369 suppression_count += 1
370 proc += ["--suppressions=%s" % suppression_file]
372 if not suppression_count:
373 logging.warning("WARNING: NOT USING SUPPRESSIONS!")
375 logfilename = self.log_dir + ("/%s." % tool_name) + "%p"
376 if self.UseXML():
377 proc += ["--xml=yes", "--xml-file=" + logfilename]
378 else:
379 proc += ["--log-file=" + logfilename]
381 # The Valgrind command is constructed.
383 # Handle --indirect_webkit_layout separately.
384 if self._options.indirect_webkit_layout:
385 # Need to create the wrapper before modifying |proc|.
386 wrapper = self.CreateBrowserWrapper(proc, webkit=True)
387 proc = self._args
388 proc.append("--wrapper")
389 proc.append(wrapper)
390 return proc
392 if self._options.indirect:
393 wrapper = self.CreateBrowserWrapper(proc)
394 os.environ["BROWSER_WRAPPER"] = wrapper
395 logging.info('export BROWSER_WRAPPER=' + wrapper)
396 proc = []
397 proc += self._args
398 return proc
400 def ToolSpecificFlags(self):
401 raise NotImplementedError, "This method should be implemented " \
402 "in the tool-specific subclass"
404 def CreateBrowserWrapper(self, proc, webkit=False):
405 """The program being run invokes Python or something else that can't stand
406 to be valgrinded, and also invokes the Chrome browser. In this case, use a
407 magic wrapper to only valgrind the Chrome browser. Build the wrapper here.
408 Returns the path to the wrapper. It's up to the caller to use the wrapper
409 appropriately.
411 command = " ".join(proc)
412 # Add the PID of the browser wrapper to the logfile names so we can
413 # separate log files for different UI tests at the analyze stage.
414 command = command.replace("%p", "$$.%p")
416 (fd, indirect_fname) = tempfile.mkstemp(dir=self.log_dir,
417 prefix="browser_wrapper.",
418 text=True)
419 f = os.fdopen(fd, "w")
420 f.write('#!/bin/bash\n'
421 'echo "Started Valgrind wrapper for this test, PID=$$" >&2\n')
423 f.write('DIR=`dirname $0`\n'
424 'TESTNAME_FILE=$DIR/testcase.$$.name\n\n')
426 if webkit:
427 # Webkit layout_tests pass the URL as the first line of stdin.
428 f.write('tee $TESTNAME_FILE | %s "$@"\n' % command)
429 else:
430 # Try to get the test case name by looking at the program arguments.
431 # i.e. Chromium ui_tests used --test-name arg.
432 # TODO(timurrrr): This doesn't handle "--test-name Test.Name"
433 # TODO(timurrrr): ui_tests are dead. Where do we use the non-webkit
434 # wrapper now? browser_tests? What do they do?
435 f.write('for arg in $@\ndo\n'
436 ' if [[ "$arg" =~ --test-name=(.*) ]]\n then\n'
437 ' echo ${BASH_REMATCH[1]} >$TESTNAME_FILE\n'
438 ' fi\n'
439 'done\n\n'
440 '%s "$@"\n' % command)
442 f.close()
443 os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR)
444 return indirect_fname
446 def CreateAnalyzer(self):
447 raise NotImplementedError, "This method should be implemented " \
448 "in the tool-specific subclass"
450 def GetAnalyzeResults(self, check_sanity=False):
451 # Glob all the files in the log directory
452 filenames = glob.glob(self.log_dir + "/" + self.ToolName() + ".*")
454 # If we have browser wrapper, the logfiles are named as
455 # "toolname.wrapper_PID.valgrind_PID".
456 # Let's extract the list of wrapper_PIDs and name it ppids
457 ppids = set([int(f.split(".")[-2]) \
458 for f in filenames if re.search("\.[0-9]+\.[0-9]+$", f)])
460 analyzer = self.CreateAnalyzer()
461 if len(ppids) == 0:
462 # Fast path - no browser wrapper was set.
463 return analyzer.Report(filenames, None, check_sanity)
465 ret = 0
466 for ppid in ppids:
467 testcase_name = None
468 try:
469 f = open(self.log_dir + ("/testcase.%d.name" % ppid))
470 testcase_name = f.read().strip()
471 f.close()
472 wk_layout_prefix="third_party/WebKit/LayoutTests/"
473 wk_prefix_at = testcase_name.rfind(wk_layout_prefix)
474 if wk_prefix_at != -1:
475 testcase_name = testcase_name[wk_prefix_at + len(wk_layout_prefix):]
476 except IOError:
477 pass
478 print "====================================================="
479 print " Below is the report for valgrind wrapper PID=%d." % ppid
480 if testcase_name:
481 print " It was used while running the `%s` test." % testcase_name
482 else:
483 print " You can find the corresponding test"
484 print " by searching the above log for 'PID=%d'" % ppid
485 sys.stdout.flush()
487 ppid_filenames = [f for f in filenames \
488 if re.search("\.%d\.[0-9]+$" % ppid, f)]
489 # check_sanity won't work with browser wrappers
490 assert check_sanity == False
491 ret |= analyzer.Report(ppid_filenames, testcase_name)
492 print "====================================================="
493 sys.stdout.flush()
495 if ret != 0:
496 print ""
497 print "The Valgrind reports are grouped by test names."
498 print "Each test has its PID printed in the log when the test was run"
499 print "and at the beginning of its Valgrind report."
500 print "Hint: you can search for the reports by Ctrl+F -> `=#`"
501 sys.stdout.flush()
503 return ret
506 # TODO(timurrrr): Split into a separate file.
507 class Memcheck(ValgrindTool):
508 """Memcheck
509 Dynamic memory error detector for Linux & Mac
511 http://valgrind.org/info/tools.html#memcheck
514 def __init__(self):
515 super(Memcheck, self).__init__()
516 self.RegisterOptionParserHook(Memcheck.ExtendOptionParser)
518 def ToolName(self):
519 return "memcheck"
521 def ExtendOptionParser(self, parser):
522 parser.add_option("--leak-check", "--leak_check", type="string",
523 default="yes", # --leak-check=yes is equivalent of =full
524 help="perform leak checking at the end of the run")
525 parser.add_option("", "--show_all_leaks", action="store_true",
526 default=False,
527 help="also show less blatant leaks")
528 parser.add_option("", "--track_origins", action="store_true",
529 default=False,
530 help="Show whence uninitialized bytes came. 30% slower.")
532 def ToolSpecificFlags(self):
533 ret = ["--gen-suppressions=all", "--demangle=no"]
534 ret += ["--leak-check=%s" % self._options.leak_check]
536 if self._options.show_all_leaks:
537 ret += ["--show-reachable=yes"]
538 else:
539 ret += ["--show-possibly-lost=no"]
541 if self._options.track_origins:
542 ret += ["--track-origins=yes"]
544 # TODO(glider): this is a temporary workaround for http://crbug.com/51716
545 # Let's see whether it helps.
546 if common.IsMac():
547 ret += ["--smc-check=all"]
549 return ret
551 def CreateAnalyzer(self):
552 use_gdb = common.IsMac()
553 return memcheck_analyze.MemcheckAnalyzer(self._source_dir,
554 self._options.show_all_leaks,
555 use_gdb=use_gdb)
557 def Analyze(self, check_sanity=False):
558 ret = self.GetAnalyzeResults(check_sanity)
560 if ret != 0:
561 logging.info("Please see http://dev.chromium.org/developers/how-tos/"
562 "using-valgrind for the info on Memcheck/Valgrind")
563 return ret
566 class DrMemory(BaseTool):
567 """Dr.Memory
568 Dynamic memory error detector for Windows.
570 http://dev.chromium.org/developers/how-tos/using-drmemory
571 It is not very mature at the moment, some things might not work properly.
574 def __init__(self, full_mode, pattern_mode):
575 super(DrMemory, self).__init__()
576 self.full_mode = full_mode
577 self.pattern_mode = pattern_mode
578 self.RegisterOptionParserHook(DrMemory.ExtendOptionParser)
580 def ToolName(self):
581 return "drmemory"
583 def ExtendOptionParser(self, parser):
584 parser.add_option("", "--suppressions", default=[],
585 action="append",
586 help="path to a drmemory suppression file")
587 parser.add_option("", "--follow_python", action="store_true",
588 default=False, dest="follow_python",
589 help="Monitor python child processes. If off, neither "
590 "python children nor any children of python children "
591 "will be monitored.")
592 parser.add_option("", "--indirect", action="store_true",
593 default=False,
594 help="set BROWSER_WRAPPER rather than "
595 "running Dr. Memory directly on the harness")
596 parser.add_option("", "--indirect_webkit_layout", action="store_true",
597 default=False,
598 help="set --wrapper rather than running valgrind "
599 "directly.")
600 parser.add_option("", "--use_debug", action="store_true",
601 default=False, dest="use_debug",
602 help="Run Dr. Memory debug build")
603 parser.add_option("", "--trace_children", action="store_true",
604 default=True,
605 help="TODO: default value differs from Valgrind")
607 def ToolCommand(self):
608 """Get the tool command to run."""
609 # WINHEAP is what Dr. Memory supports as there are issues w/ both
610 # jemalloc (https://github.com/DynamoRIO/drmemory/issues/320) and
611 # tcmalloc (https://github.com/DynamoRIO/drmemory/issues/314)
612 add_env = {
613 "CHROME_ALLOCATOR" : "WINHEAP",
614 "JSIMD_FORCEMMX" : "1", # https://github.com/DynamoRIO/drmemory/issues/540
616 for k,v in add_env.iteritems():
617 logging.info("export %s=%s", k, v)
618 os.putenv(k, v)
620 drmem_cmd = os.getenv("DRMEMORY_COMMAND")
621 if not drmem_cmd:
622 raise RuntimeError, "Please set DRMEMORY_COMMAND environment variable " \
623 "with the path to drmemory.exe"
624 proc = drmem_cmd.split(" ")
626 # By default, don't run python (this will exclude python's children as well)
627 # to reduce runtime. We're not really interested in spending time finding
628 # bugs in the python implementation.
629 # With file-based config we must update the file every time, and
630 # it will affect simultaneous drmem uses by this user. While file-based
631 # config has many advantages, here we may want this-instance-only
632 # (https://github.com/DynamoRIO/drmemory/issues/334).
633 drconfig_cmd = [ proc[0].replace("drmemory.exe", "drconfig.exe") ]
634 drconfig_cmd += ["-quiet"] # suppress errors about no 64-bit libs
635 run_drconfig = True
636 if self._options.follow_python:
637 logging.info("Following python children")
638 # -unreg fails if not already registered so query for that first
639 query_cmd = drconfig_cmd + ["-isreg", "python.exe"]
640 query_proc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE,
641 shell=True)
642 (query_out, query_err) = query_proc.communicate()
643 if re.search("exe not registered", query_out):
644 run_drconfig = False # all set
645 else:
646 drconfig_cmd += ["-unreg", "python.exe"]
647 else:
648 logging.info("Excluding python children")
649 drconfig_cmd += ["-reg", "python.exe", "-norun"]
650 if run_drconfig:
651 drconfig_retcode = common.RunSubprocess(drconfig_cmd, self._timeout)
652 if drconfig_retcode:
653 logging.error("Configuring whether to follow python children failed " \
654 "with %d.", drconfig_retcode)
655 raise RuntimeError, "Configuring python children failed "
657 suppression_count = 0
658 supp_files = self._options.suppressions
659 if self.full_mode:
660 supp_files += [s.replace(".txt", "_full.txt") for s in supp_files]
661 for suppression_file in supp_files:
662 if os.path.exists(suppression_file):
663 suppression_count += 1
664 proc += ["-suppress", common.NormalizeWindowsPath(suppression_file)]
666 if not suppression_count:
667 logging.warning("WARNING: NOT USING SUPPRESSIONS!")
669 # Un-comment to dump Dr.Memory events on error
670 #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"]
672 # Un-comment and comment next line to debug Dr.Memory
673 #proc += ["-dr_ops", "-no_hide"]
674 #proc += ["-dr_ops", "-msgbox_mask", "-dr_ops", "15"]
675 #Proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "15"]
676 # Ensure we see messages about Dr. Memory crashing!
677 proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "12"]
679 if self._options.use_debug:
680 proc += ["-debug"]
682 proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)]
684 if self.log_parent_dir:
685 # gpu process on Windows Vista+ runs at Low Integrity and can only
686 # write to certain directories (http://crbug.com/119131)
687 symcache_dir = os.path.join(self.log_parent_dir, "drmemory.symcache")
688 elif self._options.build_dir:
689 # The other case is only possible with -t cmdline.
690 # Anyways, if we omit -symcache_dir the -logdir's value is used which
691 # should be fine.
692 symcache_dir = os.path.join(self._options.build_dir, "drmemory.symcache")
693 if symcache_dir:
694 if not os.path.exists(symcache_dir):
695 try:
696 os.mkdir(symcache_dir)
697 except OSError:
698 logging.warning("Can't create symcache dir?")
699 if os.path.exists(symcache_dir):
700 proc += ["-symcache_dir", common.NormalizeWindowsPath(symcache_dir)]
702 # Use -no_summary to suppress DrMemory's summary and init-time
703 # notifications. We generate our own with drmemory_analyze.py.
704 proc += ["-batch", "-no_summary"]
706 # Un-comment to disable interleaved output. Will also suppress error
707 # messages normally printed to stderr.
708 #proc += ["-quiet", "-no_results_to_stderr"]
710 proc += ["-callstack_max_frames", "40"]
712 # disable leak scan for now
713 proc += ["-no_count_leaks", "-no_leak_scan"]
715 # disable warnings about unaddressable prefetches
716 proc += ["-no_check_prefetch"]
718 # crbug.com/413215, no heap mismatch check for Windows release build binary
719 if common.IsWindows() and "Release" in self._options.build_dir:
720 proc += ["-no_check_delete_mismatch"]
722 # make callstacks easier to read
723 proc += ["-callstack_srcfile_prefix",
724 "build\\src,chromium\\src,crt_build\\self_x86"]
725 proc += ["-callstack_modname_hide",
726 "*drmemory*,chrome.dll"]
728 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
729 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
730 proc += ["-callstack_truncate_below", ",".join(boring_callers)]
732 if self.pattern_mode:
733 proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"]
734 elif not self.full_mode:
735 proc += ["-light"]
737 proc += self._tool_flags
739 # Dr.Memory requires -- to separate tool flags from the executable name.
740 proc += ["--"]
742 if self._options.indirect or self._options.indirect_webkit_layout:
743 wrapper_path = os.path.join(self._source_dir,
744 "tools", "valgrind", "browser_wrapper_win.py")
745 wrapper = " ".join(["python", wrapper_path] + proc)
746 self.CreateBrowserWrapper(wrapper)
747 logging.info("browser wrapper = " + " ".join(proc))
748 if self._options.indirect_webkit_layout:
749 proc = self._args
750 # Layout tests want forward slashes.
751 wrapper = wrapper.replace('\\', '/')
752 proc += ["--wrapper", wrapper]
753 return proc
754 else:
755 proc = []
757 # Note that self._args begins with the name of the exe to be run.
758 self._args[0] = common.NormalizeWindowsPath(self._args[0])
759 proc += self._args
760 return proc
762 def CreateBrowserWrapper(self, command):
763 os.putenv("BROWSER_WRAPPER", command)
765 def Analyze(self, check_sanity=False):
766 # Use one analyzer for all the log files to avoid printing duplicate reports
768 # TODO(timurrrr): unify this with Valgrind and other tools when we have
769 # https://github.com/DynamoRIO/drmemory/issues/684
770 analyzer = drmemory_analyze.DrMemoryAnalyzer()
772 ret = 0
773 if not self._options.indirect and not self._options.indirect_webkit_layout:
774 filenames = glob.glob(self.log_dir + "/*/results.txt")
776 ret = analyzer.Report(filenames, None, check_sanity)
777 else:
778 testcases = glob.glob(self.log_dir + "/testcase.*.logs")
779 # If we have browser wrapper, the per-test logdirs are named as
780 # "testcase.wrapper_PID.name".
781 # Let's extract the list of wrapper_PIDs and name it ppids.
782 # NOTE: ppids may contain '_', i.e. they are not ints!
783 ppids = set([f.split(".")[-2] for f in testcases])
785 for ppid in ppids:
786 testcase_name = None
787 try:
788 f = open("%s/testcase.%s.name" % (self.log_dir, ppid))
789 testcase_name = f.read().strip()
790 f.close()
791 except IOError:
792 pass
793 print "====================================================="
794 print " Below is the report for drmemory wrapper PID=%s." % ppid
795 if testcase_name:
796 print " It was used while running the `%s` test." % testcase_name
797 else:
798 # TODO(timurrrr): hm, the PID line is suppressed on Windows...
799 print " You can find the corresponding test"
800 print " by searching the above log for 'PID=%s'" % ppid
801 sys.stdout.flush()
802 ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" %
803 (self.log_dir, ppid))
804 ret |= analyzer.Report(ppid_filenames, testcase_name, False)
805 print "====================================================="
806 sys.stdout.flush()
808 logging.info("Please see http://dev.chromium.org/developers/how-tos/"
809 "using-drmemory for the info on Dr. Memory")
810 return ret
813 class ToolFactory:
814 def Create(self, tool_name):
815 if tool_name == "memcheck":
816 return Memcheck()
817 if tool_name == "drmemory" or tool_name == "drmemory_light":
818 # TODO(timurrrr): remove support for "drmemory" when buildbots are
819 # switched to drmemory_light OR make drmemory==drmemory_full the default
820 # mode when the tool is mature enough.
821 return DrMemory(False, False)
822 if tool_name == "drmemory_full":
823 return DrMemory(True, False)
824 if tool_name == "drmemory_pattern":
825 return DrMemory(False, True)
826 try:
827 platform_name = common.PlatformNames()[0]
828 except common.NotImplementedError:
829 platform_name = sys.platform + "(Unknown)"
830 raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name,
831 platform_name)
833 def CreateTool(tool):
834 return ToolFactory().Create(tool)