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 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.
527class 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
586class 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
621class 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
722class 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
742class 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
792class 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.
1039class 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
1061class 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
1084class 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
1108class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin, ThreadSanitizerPosix):
1109  pass
1110
1111
1112class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin, ThreadSanitizerPosix):
1113  pass
1114
1115
1116class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin,
1117                                ThreadSanitizerWindows):
1118  pass
1119
1120
1121class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin,
1122                                ThreadSanitizerWindows):
1123  pass
1124
1125
1126class 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
1168class EmbeddedTool(BaseTool):
1169  """Abstract class for tools embedded directly into the test binary.
1170  """
1171  # TODO(glider): need to override Execute() and support process chaining here.
1172
1173  def ToolCommand(self):
1174    # In the simplest case just the args of the script.
1175    return self._args
1176
1177
1178class Asan(EmbeddedTool):
1179  """AddressSanitizer, a memory error detector.
1180
1181  More information at
1182  http://dev.chromium.org/developers/testing/addresssanitizer
1183  """
1184  def __init__(self):
1185    super(Asan, self).__init__()
1186    self._timeout = 1200
1187    if common.IsMac():
1188      self._env["DYLD_NO_PIE"] = "1"
1189
1190
1191  def ToolName(self):
1192    return "asan"
1193
1194  def ToolCommand(self):
1195    # TODO(glider): use pipes instead of the ugly wrapper here once they
1196    # are supported.
1197    procs = [os.path.join(self._source_dir, "tools", "valgrind",
1198                              "asan", "asan_wrapper.sh")]
1199    procs.extend(self._args)
1200    return procs
1201
1202  def Analyze(sels, unused_check_sanity):
1203    return 0
1204
1205
1206class ToolFactory:
1207  def Create(self, tool_name):
1208    if tool_name == "memcheck":
1209      return Memcheck()
1210    if tool_name == "tsan":
1211      if common.IsWindows():
1212        return ThreadSanitizerWindows()
1213      else:
1214        return ThreadSanitizerPosix()
1215    if tool_name == "drmemory" or tool_name == "drmemory_light":
1216      # TODO(timurrrr): remove support for "drmemory" when buildbots are
1217      # switched to drmemory_light OR make drmemory==drmemory_full the default
1218      # mode when the tool is mature enough.
1219      return DrMemory(False, False)
1220    if tool_name == "drmemory_full":
1221      return DrMemory(True, False)
1222    if tool_name == "drmemory_pattern":
1223      return DrMemory(False, True)
1224    if tool_name == "tsan_rv":
1225      return RaceVerifier()
1226    if tool_name == "asan":
1227      return Asan()
1228    try:
1229      platform_name = common.PlatformNames()[0]
1230    except common.NotImplementedError:
1231      platform_name = sys.platform + "(Unknown)"
1232    raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name,
1233                                                                 platform_name)
1234
1235def CreateTool(tool):
1236  return ToolFactory().Create(tool)
1237