Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / tools / valgrind / valgrind_test.py
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 from 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     # crbug.com/413215, no heap mismatch check for Windows release build binary
942     if common.IsWindows() and "Release" in self._options.build_dir:
943         proc += ["-no_check_delete_mismatch"]
944
945     # make callstacks easier to read
946     proc += ["-callstack_srcfile_prefix",
947              "build\\src,chromium\\src,crt_build\\self_x86"]
948     proc += ["-callstack_modname_hide",
949              "*drmemory*,chrome.dll"]
950
951     boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
952     # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
953     proc += ["-callstack_truncate_below", ",".join(boring_callers)]
954
955     if self.pattern_mode:
956       proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"]
957     elif not self.full_mode:
958       proc += ["-light"]
959
960     proc += self._tool_flags
961
962     # Dr.Memory requires -- to separate tool flags from the executable name.
963     proc += ["--"]
964
965     if self._options.indirect or self._options.indirect_webkit_layout:
966       # TODO(timurrrr): reuse for TSan on Windows
967       wrapper_path = os.path.join(self._source_dir,
968                                   "tools", "valgrind", "browser_wrapper_win.py")
969       wrapper = " ".join(["python", wrapper_path] + proc)
970       self.CreateBrowserWrapper(wrapper)
971       logging.info("browser wrapper = " + " ".join(proc))
972       if self._options.indirect_webkit_layout:
973         proc = self._args
974         # Layout tests want forward slashes.
975         wrapper = wrapper.replace('\\', '/')
976         proc += ["--wrapper", wrapper]
977         return proc
978       else:
979         proc = []
980
981     # Note that self._args begins with the name of the exe to be run.
982     self._args[0] = common.NormalizeWindowsPath(self._args[0])
983     proc += self._args
984     return proc
985
986   def CreateBrowserWrapper(self, command):
987     os.putenv("BROWSER_WRAPPER", command)
988
989   def Analyze(self, check_sanity=False):
990     # Use one analyzer for all the log files to avoid printing duplicate reports
991     #
992     # TODO(timurrrr): unify this with Valgrind and other tools when we have
993     # http://code.google.com/p/drmemory/issues/detail?id=684
994     analyzer = drmemory_analyze.DrMemoryAnalyzer()
995
996     ret = 0
997     if not self._options.indirect and not self._options.indirect_webkit_layout:
998       filenames = glob.glob(self.log_dir + "/*/results.txt")
999
1000       ret = analyzer.Report(filenames, None, check_sanity)
1001     else:
1002       testcases = glob.glob(self.log_dir + "/testcase.*.logs")
1003       # If we have browser wrapper, the per-test logdirs are named as
1004       # "testcase.wrapper_PID.name".
1005       # Let's extract the list of wrapper_PIDs and name it ppids.
1006       # NOTE: ppids may contain '_', i.e. they are not ints!
1007       ppids = set([f.split(".")[-2] for f in testcases])
1008
1009       for ppid in ppids:
1010         testcase_name = None
1011         try:
1012           f = open("%s/testcase.%s.name" % (self.log_dir, ppid))
1013           testcase_name = f.read().strip()
1014           f.close()
1015         except IOError:
1016           pass
1017         print "====================================================="
1018         print " Below is the report for drmemory wrapper PID=%s." % ppid
1019         if testcase_name:
1020           print " It was used while running the `%s` test." % testcase_name
1021         else:
1022           # TODO(timurrrr): hm, the PID line is suppressed on Windows...
1023           print " You can find the corresponding test"
1024           print " by searching the above log for 'PID=%s'" % ppid
1025         sys.stdout.flush()
1026         ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" %
1027                                    (self.log_dir, ppid))
1028         ret |= analyzer.Report(ppid_filenames, testcase_name, False)
1029         print "====================================================="
1030         sys.stdout.flush()
1031
1032     logging.info("Please see http://dev.chromium.org/developers/how-tos/"
1033                  "using-drmemory for the info on Dr. Memory")
1034     return ret
1035
1036
1037 # RaceVerifier support. See
1038 # http://code.google.com/p/data-race-test/wiki/RaceVerifier for more details.
1039 class ThreadSanitizerRV1Analyzer(tsan_analyze.TsanAnalyzer):
1040   """ TsanAnalyzer that saves race reports to a file. """
1041
1042   TMP_FILE = "rvlog.tmp"
1043
1044   def __init__(self, source_dir, use_gdb):
1045     super(ThreadSanitizerRV1Analyzer, self).__init__(use_gdb)
1046     self.out = open(self.TMP_FILE, "w")
1047
1048   def Report(self, files, testcase, check_sanity=False):
1049     reports = self.GetReports(files)
1050     for report in reports:
1051       print >>self.out, report
1052     if len(reports) > 0:
1053       logging.info("RaceVerifier pass 1 of 2, found %i reports" % len(reports))
1054       return -1
1055     return 0
1056
1057   def CloseOutputFile(self):
1058     self.out.close()
1059
1060
1061 class ThreadSanitizerRV1Mixin(object):
1062   """RaceVerifier first pass.
1063
1064   Runs ThreadSanitizer as usual, but hides race reports and collects them in a
1065   temporary file"""
1066
1067   def __init__(self):
1068     super(ThreadSanitizerRV1Mixin, self).__init__()
1069     self.RegisterOptionParserHook(ThreadSanitizerRV1Mixin.ExtendOptionParser)
1070
1071   def ExtendOptionParser(self, parser):
1072     parser.set_defaults(hybrid="yes")
1073
1074   def CreateAnalyzer(self):
1075     use_gdb = common.IsMac()
1076     self.analyzer = ThreadSanitizerRV1Analyzer(self._source_dir, use_gdb)
1077     return self.analyzer
1078
1079   def Cleanup(self):
1080     super(ThreadSanitizerRV1Mixin, self).Cleanup()
1081     self.analyzer.CloseOutputFile()
1082
1083
1084 class ThreadSanitizerRV2Mixin(object):
1085   """RaceVerifier second pass."""
1086
1087   def __init__(self):
1088     super(ThreadSanitizerRV2Mixin, self).__init__()
1089     self.RegisterOptionParserHook(ThreadSanitizerRV2Mixin.ExtendOptionParser)
1090
1091   def ExtendOptionParser(self, parser):
1092     parser.add_option("", "--race-verifier-sleep-ms",
1093                             dest="race_verifier_sleep_ms", default=10,
1094                             help="duration of RaceVerifier delays")
1095
1096   def ToolSpecificFlags(self):
1097     proc = super(ThreadSanitizerRV2Mixin, self).ToolSpecificFlags()
1098     proc += ['--race-verifier=%s' % ThreadSanitizerRV1Analyzer.TMP_FILE,
1099              '--race-verifier-sleep-ms=%d' %
1100              int(self._options.race_verifier_sleep_ms)]
1101     return proc
1102
1103   def Cleanup(self):
1104     super(ThreadSanitizerRV2Mixin, self).Cleanup()
1105     os.unlink(ThreadSanitizerRV1Analyzer.TMP_FILE)
1106
1107
1108 class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin, ThreadSanitizerPosix):
1109   pass
1110
1111
1112 class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin, ThreadSanitizerPosix):
1113   pass
1114
1115
1116 class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin,
1117                                 ThreadSanitizerWindows):
1118   pass
1119
1120
1121 class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin,
1122                                 ThreadSanitizerWindows):
1123   pass
1124
1125
1126 class RaceVerifier(object):
1127   """Runs tests under RaceVerifier/Valgrind."""
1128
1129   MORE_INFO_URL = "http://code.google.com/p/data-race-test/wiki/RaceVerifier"
1130
1131   def RV1Factory(self):
1132     if common.IsWindows():
1133       return ThreadSanitizerRV1Windows()
1134     else:
1135       return ThreadSanitizerRV1Posix()
1136
1137   def RV2Factory(self):
1138     if common.IsWindows():
1139       return ThreadSanitizerRV2Windows()
1140     else:
1141       return ThreadSanitizerRV2Posix()
1142
1143   def ToolName(self):
1144     return "tsan"
1145
1146   def Main(self, args, check_sanity, min_runtime_in_seconds):
1147     logging.info("Running a TSan + RaceVerifier test. For more information, " +
1148                  "see " + self.MORE_INFO_URL)
1149     cmd1 = self.RV1Factory()
1150     ret = cmd1.Main(args, check_sanity, min_runtime_in_seconds)
1151     # Verify race reports, if there are any.
1152     if ret == -1:
1153       logging.info("Starting pass 2 of 2. Running the same binary in " +
1154                    "RaceVerifier mode to confirm possible race reports.")
1155       logging.info("For more information, see " + self.MORE_INFO_URL)
1156       cmd2 = self.RV2Factory()
1157       ret = cmd2.Main(args, check_sanity, min_runtime_in_seconds)
1158     else:
1159       logging.info("No reports, skipping RaceVerifier second pass")
1160     logging.info("Please see " + self.MORE_INFO_URL + " for more information " +
1161                  "on RaceVerifier")
1162     return ret
1163
1164   def Run(self, args, module, min_runtime_in_seconds=0):
1165    return self.Main(args, False, min_runtime_in_seconds)
1166
1167
1168 class ToolFactory:
1169   def Create(self, tool_name):
1170     if tool_name == "memcheck":
1171       return Memcheck()
1172     if tool_name == "tsan":
1173       if common.IsWindows():
1174         return ThreadSanitizerWindows()
1175       else:
1176         return ThreadSanitizerPosix()
1177     if tool_name == "drmemory" or tool_name == "drmemory_light":
1178       # TODO(timurrrr): remove support for "drmemory" when buildbots are
1179       # switched to drmemory_light OR make drmemory==drmemory_full the default
1180       # mode when the tool is mature enough.
1181       return DrMemory(False, False)
1182     if tool_name == "drmemory_full":
1183       return DrMemory(True, False)
1184     if tool_name == "drmemory_pattern":
1185       return DrMemory(False, True)
1186     if tool_name == "tsan_rv":
1187       return RaceVerifier()
1188     try:
1189       platform_name = common.PlatformNames()[0]
1190     except common.NotImplementedError:
1191       platform_name = sys.platform + "(Unknown)"
1192     raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name,
1193                                                                  platform_name)
1194
1195 def CreateTool(tool):
1196   return ToolFactory().Create(tool)