valgrind_test.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Runs an exe through Valgrind and puts the intermediate files in a
6directory.
7"""
8
9import datetime
10import glob
11import logging
12import optparse
13import os
14import re
15import shutil
16import stat
17import subprocess
18import sys
19import tempfile
20
21import common
22
23import drmemory_analyze
24import memcheck_analyze
25import tsan_analyze
26
27class BaseTool(object):
28  """Abstract class for running Valgrind-, PIN-based and other dynamic
29  error detector tools.
30
31  Always subclass this and implement ToolCommand with framework- and
32  tool-specific stuff.
33  """
34
35  def __init__(self):
36    temp_parent_dir = None
37    self.log_parent_dir = ""
38    if common.IsWindows():
39      # gpu process on Windows Vista+ runs at Low Integrity and can only
40      # write to certain directories (http://crbug.com/119131)
41      #
42      # TODO(bruening): if scripts die in middle and don't clean up temp
43      # dir, we'll accumulate files in profile dir.  should remove
44      # really old files automatically.
45      profile = os.getenv("USERPROFILE")
46      if profile:
47        self.log_parent_dir = profile + "\\AppData\\LocalLow\\"
48        if os.path.exists(self.log_parent_dir):
49          self.log_parent_dir = common.NormalizeWindowsPath(self.log_parent_dir)
50          temp_parent_dir = self.log_parent_dir
51    # Generated every time (even when overridden)
52    self.temp_dir = tempfile.mkdtemp(prefix="vg_logs_", dir=temp_parent_dir)
53    self.log_dir = self.temp_dir # overridable by --keep_logs
54    self.option_parser_hooks = []
55    # TODO(glider): we may not need some of the env vars on some of the
56    # platforms.
57    self._env = {
58      "G_SLICE" : "always-malloc",
59      "NSS_DISABLE_UNLOAD" : "1",
60      "NSS_DISABLE_ARENA_FREE_LIST" : "1",
61      "GTEST_DEATH_TEST_USE_FORK": "1",
62    }
63
64  def ToolName(self):
65    raise NotImplementedError, "This method should be implemented " \
66                               "in the tool-specific subclass"
67
68  def Analyze(self, check_sanity=False):
69    raise NotImplementedError, "This method should be implemented " \
70                               "in the tool-specific subclass"
71
72  def RegisterOptionParserHook(self, hook):
73    # Frameworks and tools can add their own flags to the parser.
74    self.option_parser_hooks.append(hook)
75
76  def CreateOptionParser(self):
77    # Defines Chromium-specific flags.
78    self._parser = optparse.OptionParser("usage: %prog [options] <program to "
79                                         "test>")
80    self._parser.disable_interspersed_args()
81    self._parser.add_option("-t", "--timeout",
82                      dest="timeout", metavar="TIMEOUT", default=10000,
83                      help="timeout in seconds for the run (default 10000)")
84    self._parser.add_option("", "--build_dir",
85                            help="the location of the compiler output")
86    self._parser.add_option("", "--source_dir",
87                            help="path to top of source tree for this build"
88                                 "(used to normalize source paths in baseline)")
89    self._parser.add_option("", "--gtest_filter", default="",
90                            help="which test case to run")
91    self._parser.add_option("", "--gtest_repeat",
92                            help="how many times to run each test")
93    self._parser.add_option("", "--gtest_print_time", action="store_true",
94                            default=False,
95                            help="show how long each test takes")
96    self._parser.add_option("", "--ignore_exit_code", action="store_true",
97                            default=False,
98                            help="ignore exit code of the test "
99                                 "(e.g. test failures)")
100    self._parser.add_option("", "--keep_logs", action="store_true",
101                            default=False,
102                            help="store memory tool logs in the <tool>.logs "
103                                 "directory instead of /tmp.\nThis can be "
104                                 "useful for tool developers/maintainers.\n"
105                                 "Please note that the <tool>.logs directory "
106                                 "will be clobbered on tool startup.")
107
108    # To add framework- or tool-specific flags, please add a hook using
109    # RegisterOptionParserHook in the corresponding subclass.
110    # See ValgrindTool and ThreadSanitizerBase for examples.
111    for hook in self.option_parser_hooks:
112      hook(self, self._parser)
113
114  def ParseArgv(self, args):
115    self.CreateOptionParser()
116
117    # self._tool_flags will store those tool flags which we don't parse
118    # manually in this script.
119    self._tool_flags = []
120    known_args = []
121
122    """ We assume that the first argument not starting with "-" is a program
123    name and all the following flags should be passed to the program.
124    TODO(timurrrr): customize optparse instead
125    """
126    while len(args) > 0 and args[0][:1] == "-":
127      arg = args[0]
128      if (arg == "--"):
129        break
130      if self._parser.has_option(arg.split("=")[0]):
131        known_args += [arg]
132      else:
133        self._tool_flags += [arg]
134      args = args[1:]
135
136    if len(args) > 0:
137      known_args += args
138
139    self._options, self._args = self._parser.parse_args(known_args)
140
141    self._timeout = int(self._options.timeout)
142    self._source_dir = self._options.source_dir
143    if self._options.keep_logs:
144      # log_parent_dir has trailing slash if non-empty
145      self.log_dir = self.log_parent_dir + "%s.logs" % self.ToolName()
146      if os.path.exists(self.log_dir):
147        shutil.rmtree(self.log_dir)
148      os.mkdir(self.log_dir)
149      logging.info("Logs are in " + self.log_dir)
150
151    self._ignore_exit_code = self._options.ignore_exit_code
152    if self._options.gtest_filter != "":
153      self._args.append("--gtest_filter=%s" % self._options.gtest_filter)
154    if self._options.gtest_repeat:
155      self._args.append("--gtest_repeat=%s" % self._options.gtest_repeat)
156    if self._options.gtest_print_time:
157      self._args.append("--gtest_print_time")
158
159    return True
160
161  def Setup(self, args):
162    return self.ParseArgv(args)
163
164  def ToolCommand(self):
165    raise NotImplementedError, "This method should be implemented " \
166                               "in the tool-specific subclass"
167
168  def Cleanup(self):
169    # You may override it in the tool-specific subclass
170    pass
171
172  def Execute(self):
173    """ Execute the app to be tested after successful instrumentation.
174    Full execution command-line provided by subclassers via proc."""
175    logging.info("starting execution...")
176    proc = self.ToolCommand()
177    for var in self._env:
178      common.PutEnvAndLog(var, self._env[var])
179    return common.RunSubprocess(proc, self._timeout)
180
181  def RunTestsAndAnalyze(self, check_sanity):
182    exec_retcode = self.Execute()
183    analyze_retcode = self.Analyze(check_sanity)
184
185    if analyze_retcode:
186      logging.error("Analyze failed.")
187      logging.info("Search the log for '[ERROR]' to see the error reports.")
188      return analyze_retcode
189
190    if exec_retcode:
191      if self._ignore_exit_code:
192        logging.info("Test execution failed, but the exit code is ignored.")
193      else:
194        logging.error("Test execution failed.")
195        return exec_retcode
196    else:
197      logging.info("Test execution completed successfully.")
198
199    if not analyze_retcode:
200      logging.info("Analysis completed successfully.")
201
202    return 0
203
204  def Main(self, args, check_sanity, min_runtime_in_seconds):
205    """Call this to run through the whole process: Setup, Execute, Analyze"""
206    start_time = datetime.datetime.now()
207    retcode = -1
208    if self.Setup(args):
209      retcode = self.RunTestsAndAnalyze(check_sanity)
210      shutil.rmtree(self.temp_dir, ignore_errors=True)
211      self.Cleanup()
212    else:
213      logging.error("Setup failed")
214    end_time = datetime.datetime.now()
215    runtime_in_seconds = (end_time - start_time).seconds
216    hours = runtime_in_seconds / 3600
217    seconds = runtime_in_seconds % 3600
218    minutes = seconds / 60
219    seconds = seconds % 60
220    logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds))
221    if (min_runtime_in_seconds > 0 and
222        runtime_in_seconds < min_runtime_in_seconds):
223      logging.error("Layout tests finished too quickly. "
224                    "It should have taken at least %d seconds. "
225                    "Something went wrong?" % min_runtime_in_seconds)
226      retcode = -1
227    return retcode
228
229  def Run(self, args, module, min_runtime_in_seconds=0):
230    MODULES_TO_SANITY_CHECK = ["base"]
231
232    # TODO(timurrrr): this is a temporary workaround for http://crbug.com/47844
233    if self.ToolName() == "tsan" and common.IsMac():
234      MODULES_TO_SANITY_CHECK = []
235
236    check_sanity = module in MODULES_TO_SANITY_CHECK
237    return self.Main(args, check_sanity, min_runtime_in_seconds)
238
239
240class ValgrindTool(BaseTool):
241  """Abstract class for running Valgrind tools.
242
243  Always subclass this and implement ToolSpecificFlags() and
244  ExtendOptionParser() for tool-specific stuff.
245  """
246  def __init__(self):
247    super(ValgrindTool, self).__init__()
248    self.RegisterOptionParserHook(ValgrindTool.ExtendOptionParser)
249
250  def UseXML(self):
251    # Override if tool prefers nonxml output
252    return True
253
254  def SelfContained(self):
255    # Returns true iff the tool is distibuted as a self-contained
256    # .sh script (e.g. ThreadSanitizer)
257    return False
258
259  def ExtendOptionParser(self, parser):
260    parser.add_option("", "--suppressions", default=[],
261                            action="append",
262                            help="path to a valgrind suppression file")
263    parser.add_option("", "--indirect", action="store_true",
264                            default=False,
265                            help="set BROWSER_WRAPPER rather than "
266                                 "running valgrind directly")
267    parser.add_option("", "--indirect_webkit_layout", action="store_true",
268                            default=False,
269                            help="set --wrapper rather than running valgrind "
270                                 "directly.")
271    parser.add_option("", "--trace_children", action="store_true",
272                            default=False,
273                            help="also trace child processes")
274    parser.add_option("", "--num-callers",
275                            dest="num_callers", default=30,
276                            help="number of callers to show in stack traces")
277    parser.add_option("", "--generate_dsym", action="store_true",
278                          default=False,
279                          help="Generate .dSYM file on Mac if needed. Slow!")
280
281  def Setup(self, args):
282    if not BaseTool.Setup(self, args):
283      return False
284    if common.IsMac():
285      self.PrepareForTestMac()
286    return True
287
288  def PrepareForTestMac(self):
289    """Runs dsymutil if needed.
290
291    Valgrind for Mac OS X requires that debugging information be in a .dSYM
292    bundle generated by dsymutil.  It is not currently able to chase DWARF
293    data into .o files like gdb does, so executables without .dSYM bundles or
294    with the Chromium-specific "fake_dsym" bundles generated by
295    build/mac/strip_save_dsym won't give source file and line number
296    information in valgrind.
297
298    This function will run dsymutil if the .dSYM bundle is missing or if
299    it looks like a fake_dsym.  A non-fake dsym that already exists is assumed
300    to be up-to-date.
301    """
302    test_command = self._args[0]
303    dsym_bundle = self._args[0] + '.dSYM'
304    dsym_file = os.path.join(dsym_bundle, 'Contents', 'Resources', 'DWARF',
305                             os.path.basename(test_command))
306    dsym_info_plist = os.path.join(dsym_bundle, 'Contents', 'Info.plist')
307
308    needs_dsymutil = True
309    saved_test_command = None
310
311    if os.path.exists(dsym_file) and os.path.exists(dsym_info_plist):
312      # Look for the special fake_dsym tag in dsym_info_plist.
313      dsym_info_plist_contents = open(dsym_info_plist).read()
314
315      if not re.search('^\s*<key>fake_dsym</key>$', dsym_info_plist_contents,
316                       re.MULTILINE):
317        # fake_dsym is not set, this is a real .dSYM bundle produced by
318        # dsymutil.  dsymutil does not need to be run again.
319        needs_dsymutil = False
320      else:
321        # fake_dsym is set.  dsym_file is a copy of the original test_command
322        # before it was stripped.  Copy it back to test_command so that
323        # dsymutil has unstripped input to work with.  Move the stripped
324        # test_command out of the way, it will be restored when this is
325        # done.
326        saved_test_command = test_command + '.stripped'
327        os.rename(test_command, saved_test_command)
328        shutil.copyfile(dsym_file, test_command)
329        shutil.copymode(saved_test_command, test_command)
330
331    if needs_dsymutil:
332      if self._options.generate_dsym:
333        # Remove the .dSYM bundle if it exists.
334        shutil.rmtree(dsym_bundle, True)
335
336        dsymutil_command = ['dsymutil', test_command]
337
338        # dsymutil is crazy slow.  Ideally we'd have a timeout here,
339        # but common.RunSubprocess' timeout is only checked
340        # after each line of output; dsymutil is silent
341        # until the end, and is then killed, which is silly.
342        common.RunSubprocess(dsymutil_command)
343
344        if saved_test_command:
345          os.rename(saved_test_command, test_command)
346      else:
347        logging.info("No real .dSYM for test_command.  Line numbers will "
348                     "not be shown.  Either tell xcode to generate .dSYM "
349                     "file, or use --generate_dsym option to this tool.")
350
351  def ToolCommand(self):
352    """Get the valgrind command to run."""
353    # Note that self._args begins with the exe to be run.
354    tool_name = self.ToolName()
355
356    # Construct the valgrind command.
357    if self.SelfContained():
358      proc = ["valgrind-%s.sh" % tool_name]
359    else:
360      proc = ["valgrind", "--tool=%s" % tool_name]
361
362    proc += ["--num-callers=%i" % int(self._options.num_callers)]
363
364    if self._options.trace_children:
365      proc += ["--trace-children=yes"]
366      proc += ["--trace-children-skip='*dbus-daemon*'"]
367      proc += ["--trace-children-skip='*dbus-launch*'"]
368      proc += ["--trace-children-skip='*perl*'"]
369      proc += ["--trace-children-skip='*python*'"]
370
371    proc += self.ToolSpecificFlags()
372    proc += self._tool_flags
373
374    suppression_count = 0
375    for suppression_file in self._options.suppressions:
376      if os.path.exists(suppression_file):
377        suppression_count += 1
378        proc += ["--suppressions=%s" % suppression_file]
379
380    if not suppression_count:
381      logging.warning("WARNING: NOT USING SUPPRESSIONS!")
382
383    logfilename = self.log_dir + ("/%s." % tool_name) + "%p"
384    if self.UseXML():
385      proc += ["--xml=yes", "--xml-file=" + logfilename]
386    else:
387      proc += ["--log-file=" + logfilename]
388
389    # The Valgrind command is constructed.
390
391    # Handle --indirect_webkit_layout separately.
392    if self._options.indirect_webkit_layout:
393      # Need to create the wrapper before modifying |proc|.
394      wrapper = self.CreateBrowserWrapper(proc, webkit=True)
395      proc = self._args
396      proc.append("--wrapper")
397      proc.append(wrapper)
398      return proc
399
400    # Valgrind doesn't play nice with the Chrome sandbox.  Empty this env var
401    # set by runtest.py to disable the sandbox.
402    if os.environ.get("CHROME_DEVEL_SANDBOX", None):
403      logging.info("Removing CHROME_DEVEL_SANDBOX fron environment")
404      os.environ["CHROME_DEVEL_SANDBOX"] = ''
405
406    if self._options.indirect:
407      wrapper = self.CreateBrowserWrapper(proc)
408      os.environ["BROWSER_WRAPPER"] = wrapper
409      logging.info('export BROWSER_WRAPPER=' + wrapper)
410      proc = []
411    proc += self._args
412    return proc
413
414  def ToolSpecificFlags(self):
415    raise NotImplementedError, "This method should be implemented " \
416                               "in the tool-specific subclass"
417
418  def CreateBrowserWrapper(self, proc, webkit=False):
419    """The program being run invokes Python or something else that can't stand
420    to be valgrinded, and also invokes the Chrome browser. In this case, use a
421    magic wrapper to only valgrind the Chrome browser. Build the wrapper here.
422    Returns the path to the wrapper. It's up to the caller to use the wrapper
423    appropriately.
424    """
425    command = " ".join(proc)
426    # Add the PID of the browser wrapper to the logfile names so we can
427    # separate log files for different UI tests at the analyze stage.
428    command = command.replace("%p", "$$.%p")
429
430    (fd, indirect_fname) = tempfile.mkstemp(dir=self.log_dir,
431                                            prefix="browser_wrapper.",
432                                            text=True)
433    f = os.fdopen(fd, "w")
434    f.write('#!/bin/bash\n'
435            'echo "Started Valgrind wrapper for this test, PID=$$" >&2\n')
436
437    f.write('DIR=`dirname $0`\n'
438            'TESTNAME_FILE=$DIR/testcase.$$.name\n\n')
439
440    if webkit:
441      # Webkit layout_tests pass the URL as the first line of stdin.
442      f.write('tee $TESTNAME_FILE | %s "$@"\n' % command)
443    else:
444      # Try to get the test case name by looking at the program arguments.
445      # i.e. Chromium ui_tests used --test-name arg.
446      # TODO(timurrrr): This doesn't handle "--test-name Test.Name"
447      # TODO(timurrrr): ui_tests are dead. Where do we use the non-webkit
448      # wrapper now? browser_tests? What do they do?
449      f.write('for arg in $@\ndo\n'
450              '  if [[ "$arg" =~ --test-name=(.*) ]]\n  then\n'
451              '    echo ${BASH_REMATCH[1]} >$TESTNAME_FILE\n'
452              '  fi\n'
453              'done\n\n'
454              '%s "$@"\n' % command)
455
456    f.close()
457    os.chmod(indirect_fname, stat.S_IRUSR|stat.S_IXUSR)
458    return indirect_fname
459
460  def CreateAnalyzer(self):
461    raise NotImplementedError, "This method should be implemented " \
462                               "in the tool-specific subclass"
463
464  def GetAnalyzeResults(self, check_sanity=False):
465    # Glob all the files in the log directory
466    filenames = glob.glob(self.log_dir + "/" + self.ToolName() + ".*")
467
468    # If we have browser wrapper, the logfiles are named as
469    # "toolname.wrapper_PID.valgrind_PID".
470    # Let's extract the list of wrapper_PIDs and name it ppids
471    ppids = set([int(f.split(".")[-2]) \
472                for f in filenames if re.search("\.[0-9]+\.[0-9]+$", f)])
473
474    analyzer = self.CreateAnalyzer()
475    if len(ppids) == 0:
476      # Fast path - no browser wrapper was set.
477      return analyzer.Report(filenames, None, check_sanity)
478
479    ret = 0
480    for ppid in ppids:
481      testcase_name = None
482      try:
483        f = open(self.log_dir + ("/testcase.%d.name" % ppid))
484        testcase_name = f.read().strip()
485        f.close()
486        wk_layout_prefix="third_party/WebKit/LayoutTests/"
487        wk_prefix_at = testcase_name.rfind(wk_layout_prefix)
488        if wk_prefix_at != -1:
489          testcase_name = testcase_name[wk_prefix_at + len(wk_layout_prefix):]
490      except IOError:
491        pass
492      print "====================================================="
493      print " Below is the report for valgrind wrapper PID=%d." % ppid
494      if testcase_name:
495        print " It was used while running the `%s` test." % testcase_name
496      else:
497        print " You can find the corresponding test"
498        print " by searching the above log for 'PID=%d'" % ppid
499      sys.stdout.flush()
500
501      ppid_filenames = [f for f in filenames \
502                        if re.search("\.%d\.[0-9]+$" % ppid, f)]
503      # check_sanity won't work with browser wrappers
504      assert check_sanity == False
505      ret |= analyzer.Report(ppid_filenames, testcase_name)
506      print "====================================================="
507      sys.stdout.flush()
508
509    if ret != 0:
510      print ""
511      print "The Valgrind reports are grouped by test names."
512      print "Each test has its PID printed in the log when the test was run"
513      print "and at the beginning of its Valgrind report."
514      print "Hint: you can search for the reports by Ctrl+F -> `=#`"
515      sys.stdout.flush()
516
517    return ret
518
519
520# TODO(timurrrr): Split into a separate file.
521class Memcheck(ValgrindTool):
522  """Memcheck
523  Dynamic memory error detector for Linux & Mac
524
525  http://valgrind.org/info/tools.html#memcheck
526  """
527
528  def __init__(self):
529    super(Memcheck, self).__init__()
530    self.RegisterOptionParserHook(Memcheck.ExtendOptionParser)
531
532  def ToolName(self):
533    return "memcheck"
534
535  def ExtendOptionParser(self, parser):
536    parser.add_option("--leak-check", "--leak_check", type="string",
537                      default="yes",  # --leak-check=yes is equivalent of =full
538                      help="perform leak checking at the end of the run")
539    parser.add_option("", "--show_all_leaks", action="store_true",
540                      default=False,
541                      help="also show less blatant leaks")
542    parser.add_option("", "--track_origins", action="store_true",
543                      default=False,
544                      help="Show whence uninitialized bytes came. 30% slower.")
545
546  def ToolSpecificFlags(self):
547    ret = ["--gen-suppressions=all", "--demangle=no"]
548    ret += ["--leak-check=%s" % self._options.leak_check]
549
550    if self._options.show_all_leaks:
551      ret += ["--show-reachable=yes"]
552    else:
553      ret += ["--show-possibly-lost=no"]
554
555    if self._options.track_origins:
556      ret += ["--track-origins=yes"]
557
558    # TODO(glider): this is a temporary workaround for http://crbug.com/51716
559    # Let's see whether it helps.
560    if common.IsMac():
561      ret += ["--smc-check=all"]
562
563    return ret
564
565  def CreateAnalyzer(self):
566    use_gdb = common.IsMac()
567    return memcheck_analyze.MemcheckAnalyzer(self._source_dir,
568                                            self._options.show_all_leaks,
569                                            use_gdb=use_gdb)
570
571  def Analyze(self, check_sanity=False):
572    ret = self.GetAnalyzeResults(check_sanity)
573
574    if ret != 0:
575      logging.info("Please see http://dev.chromium.org/developers/how-tos/"
576                   "using-valgrind for the info on Memcheck/Valgrind")
577    return ret
578
579
580class PinTool(BaseTool):
581  """Abstract class for running PIN tools.
582
583  Always subclass this and implement ToolSpecificFlags() and
584  ExtendOptionParser() for tool-specific stuff.
585  """
586  def PrepareForTest(self):
587    pass
588
589  def ToolSpecificFlags(self):
590    raise NotImplementedError, "This method should be implemented " \
591                               "in the tool-specific subclass"
592
593  def ToolCommand(self):
594    """Get the PIN command to run."""
595
596    # Construct the PIN command.
597    pin_cmd = os.getenv("PIN_COMMAND")
598    if not pin_cmd:
599      raise RuntimeError, "Please set PIN_COMMAND environment variable " \
600                          "with the path to pin.exe"
601    proc = pin_cmd.split(" ")
602
603    proc += self.ToolSpecificFlags()
604
605    # The PIN command is constructed.
606
607    # PIN requires -- to separate PIN flags from the executable name.
608    # self._args begins with the exe to be run.
609    proc += ["--"]
610
611    proc += self._args
612    return proc
613
614
615class ThreadSanitizerBase(object):
616  """ThreadSanitizer
617  Dynamic data race detector for Linux, Mac and Windows.
618
619  http://code.google.com/p/data-race-test/wiki/ThreadSanitizer
620
621  Since TSan works on both Valgrind (Linux, Mac) and PIN (Windows), we need
622  to have multiple inheritance
623  """
624
625  INFO_MESSAGE="Please see http://dev.chromium.org/developers/how-tos/" \
626               "using-valgrind/threadsanitizer for the info on " \
627               "ThreadSanitizer"
628
629  def __init__(self):
630    super(ThreadSanitizerBase, self).__init__()
631    self.RegisterOptionParserHook(ThreadSanitizerBase.ExtendOptionParser)
632
633  def ToolName(self):
634    return "tsan"
635
636  def UseXML(self):
637    return False
638
639  def SelfContained(self):
640    return True
641
642  def ExtendOptionParser(self, parser):
643    parser.add_option("", "--hybrid", default="no",
644                      dest="hybrid",
645                      help="Finds more data races, may give false positive "
646                      "reports unless the code is annotated")
647    parser.add_option("", "--announce-threads", default="yes",
648                      dest="announce_threads",
649                      help="Show the the stack traces of thread creation")
650    parser.add_option("", "--free-is-write", default="no",
651                      dest="free_is_write",
652                      help="Treat free()/operator delete as memory write. "
653                      "This helps finding more data races, but (currently) "
654                      "this may give false positive reports on std::string "
655                      "internals, see http://code.google.com/p/data-race-test"
656                      "/issues/detail?id=40")
657
658  def EvalBoolFlag(self, flag_value):
659    if (flag_value in ["1", "true", "yes"]):
660      return True
661    elif (flag_value in ["0", "false", "no"]):
662      return False
663    raise RuntimeError, "Can't parse flag value (%s)" % flag_value
664
665  def ToolSpecificFlags(self):
666    ret = []
667
668    ignore_files = ["ignores.txt"]
669    for platform_suffix in common.PlatformNames():
670      ignore_files.append("ignores_%s.txt" % platform_suffix)
671    for ignore_file in ignore_files:
672      fullname =  os.path.join(self._source_dir,
673          "tools", "valgrind", "tsan", ignore_file)
674      if os.path.exists(fullname):
675        fullname = common.NormalizeWindowsPath(fullname)
676        ret += ["--ignore=%s" % fullname]
677
678    # This should shorten filepaths for local builds.
679    ret += ["--file-prefix-to-cut=%s/" % self._source_dir]
680
681    # This should shorten filepaths on bots.
682    ret += ["--file-prefix-to-cut=build/src/"]
683
684    # This should shorten filepaths for functions intercepted in TSan.
685    ret += ["--file-prefix-to-cut=scripts/tsan/tsan/"]
686
687    ret += ["--gen-suppressions=true"]
688
689    if self.EvalBoolFlag(self._options.hybrid):
690      ret += ["--hybrid=yes"] # "no" is the default value for TSAN
691
692    if self.EvalBoolFlag(self._options.announce_threads):
693      ret += ["--announce-threads"]
694
695    if self.EvalBoolFlag(self._options.free_is_write):
696      ret += ["--free-is-write=yes"]
697    else:
698      ret += ["--free-is-write=no"]
699
700
701    # --show-pc flag is needed for parsing the error logs on Darwin.
702    if platform_suffix == 'mac':
703      ret += ["--show-pc=yes"]
704    ret += ["--show-pid=no"]
705
706    boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
707    # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
708    for bc in boring_callers:
709      ret += ["--cut_stack_below=%s" % bc]
710
711    return ret
712
713
714class ThreadSanitizerPosix(ThreadSanitizerBase, ValgrindTool):
715  def ToolSpecificFlags(self):
716    proc = ThreadSanitizerBase.ToolSpecificFlags(self)
717    # The -v flag is needed for printing the list of used suppressions and
718    # obtaining addresses for loaded shared libraries on Mac.
719    proc += ["-v"]
720    return proc
721
722  def CreateAnalyzer(self):
723    use_gdb = common.IsMac()
724    return tsan_analyze.TsanAnalyzer(self._source_dir, use_gdb)
725
726  def Analyze(self, check_sanity=False):
727    ret = self.GetAnalyzeResults(check_sanity)
728
729    if ret != 0:
730      logging.info(self.INFO_MESSAGE)
731    return ret
732
733
734class ThreadSanitizerWindows(ThreadSanitizerBase, PinTool):
735
736  def __init__(self):
737    super(ThreadSanitizerWindows, self).__init__()
738    self.RegisterOptionParserHook(ThreadSanitizerWindows.ExtendOptionParser)
739
740  def ExtendOptionParser(self, parser):
741    parser.add_option("", "--suppressions", default=[],
742                      action="append",
743                      help="path to TSan suppression file")
744
745
746  def ToolSpecificFlags(self):
747    proc = ThreadSanitizerBase.ToolSpecificFlags(self)
748    # On PIN, ThreadSanitizer has its own suppression mechanism
749    # and --log-file flag which work exactly on Valgrind.
750    suppression_count = 0
751    for suppression_file in self._options.suppressions:
752      if os.path.exists(suppression_file):
753        suppression_count += 1
754        suppression_file = common.NormalizeWindowsPath(suppression_file)
755        proc += ["--suppressions=%s" % suppression_file]
756
757    if not suppression_count:
758      logging.warning("WARNING: NOT USING SUPPRESSIONS!")
759
760    logfilename = self.log_dir + "/tsan.%p"
761    proc += ["--log-file=" + common.NormalizeWindowsPath(logfilename)]
762
763    # TODO(timurrrr): Add flags for Valgrind trace children analog when we
764    # start running complex tests (e.g. UI) under TSan/Win.
765
766    return proc
767
768  def Analyze(self, check_sanity=False):
769    filenames = glob.glob(self.log_dir + "/tsan.*")
770    analyzer = tsan_analyze.TsanAnalyzer(self._source_dir)
771    ret = analyzer.Report(filenames, None, check_sanity)
772    if ret != 0:
773      logging.info(self.INFO_MESSAGE)
774    return ret
775
776
777class DrMemory(BaseTool):
778  """Dr.Memory
779  Dynamic memory error detector for Windows.
780
781  http://dev.chromium.org/developers/how-tos/using-drmemory
782  It is not very mature at the moment, some things might not work properly.
783  """
784
785  def __init__(self, full_mode, pattern_mode):
786    super(DrMemory, self).__init__()
787    self.full_mode = full_mode
788    self.pattern_mode = pattern_mode
789    self.RegisterOptionParserHook(DrMemory.ExtendOptionParser)
790
791  def ToolName(self):
792    return "drmemory"
793
794  def ExtendOptionParser(self, parser):
795    parser.add_option("", "--suppressions", default=[],
796                      action="append",
797                      help="path to a drmemory suppression file")
798    parser.add_option("", "--follow_python", action="store_true",
799                      default=False, dest="follow_python",
800                      help="Monitor python child processes.  If off, neither "
801                      "python children nor any children of python children "
802                      "will be monitored.")
803    parser.add_option("", "--indirect", action="store_true",
804                      default=False,
805                      help="set BROWSER_WRAPPER rather than "
806                           "running Dr. Memory directly on the harness")
807    parser.add_option("", "--use_debug", action="store_true",
808                      default=False, dest="use_debug",
809                      help="Run Dr. Memory debug build")
810    parser.add_option("", "--trace_children", action="store_true",
811                            default=True,
812                            help="TODO: default value differs from Valgrind")
813
814  def ToolCommand(self):
815    """Get the tool command to run."""
816    # WINHEAP is what Dr. Memory supports as there are issues w/ both
817    # jemalloc (http://code.google.com/p/drmemory/issues/detail?id=320) and
818    # tcmalloc (http://code.google.com/p/drmemory/issues/detail?id=314)
819    add_env = {
820      "CHROME_ALLOCATOR" : "WINHEAP",
821      "JSIMD_FORCEMMX"   : "1",  # http://code.google.com/p/drmemory/issues/detail?id=540
822    }
823    for k,v in add_env.iteritems():
824      logging.info("export %s=%s", k, v)
825      os.putenv(k, v)
826
827    drmem_cmd = os.getenv("DRMEMORY_COMMAND")
828    if not drmem_cmd:
829      raise RuntimeError, "Please set DRMEMORY_COMMAND environment variable " \
830                          "with the path to drmemory.exe"
831    proc = drmem_cmd.split(" ")
832
833    # By default, don't run python (this will exclude python's children as well)
834    # to reduce runtime.  We're not really interested in spending time finding
835    # bugs in the python implementation.
836    # With file-based config we must update the file every time, and
837    # it will affect simultaneous drmem uses by this user.  While file-based
838    # config has many advantages, here we may want this-instance-only
839    # (http://code.google.com/p/drmemory/issues/detail?id=334).
840    drconfig_cmd = [ proc[0].replace("drmemory.exe", "drconfig.exe") ]
841    drconfig_cmd += ["-quiet"] # suppress errors about no 64-bit libs
842    run_drconfig = True
843    if self._options.follow_python:
844      logging.info("Following python children")
845      # -unreg fails if not already registered so query for that first
846      query_cmd = drconfig_cmd + ["-isreg", "python.exe"]
847      query_proc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE,
848                                    shell=True)
849      (query_out, query_err) = query_proc.communicate()
850      if re.search("exe not registered", query_out):
851        run_drconfig = False # all set
852      else:
853        drconfig_cmd += ["-unreg", "python.exe"]
854    else:
855      logging.info("Excluding python children")
856      drconfig_cmd += ["-reg", "python.exe", "-norun"]
857    if run_drconfig:
858      drconfig_retcode = common.RunSubprocess(drconfig_cmd, self._timeout)
859      if drconfig_retcode:
860        logging.error("Configuring whether to follow python children failed " \
861                      "with %d.", drconfig_retcode)
862        raise RuntimeError, "Configuring python children failed "
863
864    suppression_count = 0
865    supp_files = self._options.suppressions
866    if self.full_mode:
867      supp_files += [s.replace(".txt", "_full.txt") for s in supp_files]
868    for suppression_file in supp_files:
869      if os.path.exists(suppression_file):
870        suppression_count += 1
871        proc += ["-suppress", common.NormalizeWindowsPath(suppression_file)]
872
873    if not suppression_count:
874      logging.warning("WARNING: NOT USING SUPPRESSIONS!")
875
876    # Un-comment to dump Dr.Memory events on error
877    #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"]
878
879    # Un-comment and comment next line to debug Dr.Memory
880    #proc += ["-dr_ops", "-no_hide"]
881    #proc += ["-dr_ops", "-msgbox_mask", "-dr_ops", "15"]
882    #Proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "15"]
883    # Ensure we see messages about Dr. Memory crashing!
884    proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "12"]
885
886    if self._options.use_debug:
887      proc += ["-debug"]
888
889    proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)]
890
891    if self.log_parent_dir:
892      # gpu process on Windows Vista+ runs at Low Integrity and can only
893      # write to certain directories (http://crbug.com/119131)
894      symcache_dir = os.path.join(self.log_parent_dir, "drmemory.symcache")
895    elif self._options.build_dir:
896      # The other case is only possible with -t cmdline.
897      # Anyways, if we omit -symcache_dir the -logdir's value is used which
898      # should be fine.
899      symcache_dir = os.path.join(self._options.build_dir, "drmemory.symcache")
900    if symcache_dir:
901      if not os.path.exists(symcache_dir):
902        try:
903          os.mkdir(symcache_dir)
904        except OSError:
905          logging.warning("Can't create symcache dir?")
906      if os.path.exists(symcache_dir):
907        proc += ["-symcache_dir", common.NormalizeWindowsPath(symcache_dir)]
908
909    # Use -no_summary to suppress DrMemory's summary and init-time
910    # notifications.  We generate our own with drmemory_analyze.py.
911    proc += ["-batch", "-no_summary"]
912
913    # Un-comment to disable interleaved output.  Will also suppress error
914    # messages normally printed to stderr.
915    #proc += ["-quiet", "-no_results_to_stderr"]
916
917    proc += ["-callstack_max_frames", "40"]
918
919    # make callstacks easier to read
920    proc += ["-callstack_srcfile_prefix",
921             "build\\src,chromium\\src,crt_build\\self_x86"]
922    proc += ["-callstack_modname_hide",
923             "*drmemory*,chrome.dll"]
924
925    boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
926    # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
927    proc += ["-callstack_truncate_below", ",".join(boring_callers)]
928
929    if self.pattern_mode:
930      proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"]
931    elif not self.full_mode:
932      proc += ["-light"]
933
934    proc += self._tool_flags
935
936    # DrM i#850/851: The new -callstack_use_top_fp_selectively has bugs.
937    proc += ["-no_callstack_use_top_fp_selectively"]
938
939    # Dr.Memory requires -- to separate tool flags from the executable name.
940    proc += ["--"]
941
942    if self._options.indirect:
943      # TODO(timurrrr): reuse for TSan on Windows
944      wrapper_path = os.path.join(self._source_dir,
945                                  "tools", "valgrind", "browser_wrapper_win.py")
946      self.CreateBrowserWrapper(" ".join(["python", wrapper_path] + proc))
947      logging.info("browser wrapper = " + " ".join(proc))
948      proc = []
949
950    # Note that self._args begins with the name of the exe to be run.
951    self._args[0] = common.NormalizeWindowsPath(self._args[0])
952    proc += self._args
953    return proc
954
955  def CreateBrowserWrapper(self, command):
956    os.putenv("BROWSER_WRAPPER", command)
957
958  def Analyze(self, check_sanity=False):
959    # Use one analyzer for all the log files to avoid printing duplicate reports
960    #
961    # TODO(timurrrr): unify this with Valgrind and other tools when we have
962    # http://code.google.com/p/drmemory/issues/detail?id=684
963    analyzer = drmemory_analyze.DrMemoryAnalyzer()
964
965    ret = 0
966    if not self._options.indirect:
967      filenames = glob.glob(self.log_dir + "/*/results.txt")
968
969      ret = analyzer.Report(filenames, None, check_sanity)
970    else:
971      testcases = glob.glob(self.log_dir + "/testcase.*.logs")
972      # If we have browser wrapper, the per-test logdirs are named as
973      # "testcase.wrapper_PID.name".
974      # Let's extract the list of wrapper_PIDs and name it ppids.
975      # NOTE: ppids may contain '_', i.e. they are not ints!
976      ppids = set([f.split(".")[-2] for f in testcases])
977
978      for ppid in ppids:
979        testcase_name = None
980        try:
981          f = open("%s/testcase.%s.name" % (self.log_dir, ppid))
982          testcase_name = f.read().strip()
983          f.close()
984        except IOError:
985          pass
986        print "====================================================="
987        print " Below is the report for drmemory wrapper PID=%s." % ppid
988        if testcase_name:
989          print " It was used while running the `%s` test." % testcase_name
990        else:
991          # TODO(timurrrr): hm, the PID line is suppressed on Windows...
992          print " You can find the corresponding test"
993          print " by searching the above log for 'PID=%s'" % ppid
994        sys.stdout.flush()
995        ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" %
996                                   (self.log_dir, ppid))
997        ret |= analyzer.Report(ppid_filenames, testcase_name, False)
998        print "====================================================="
999        sys.stdout.flush()
1000
1001    logging.info("Please see http://dev.chromium.org/developers/how-tos/"
1002                 "using-drmemory for the info on Dr. Memory")
1003    return ret
1004
1005
1006# RaceVerifier support. See
1007# http://code.google.com/p/data-race-test/wiki/RaceVerifier for more details.
1008class ThreadSanitizerRV1Analyzer(tsan_analyze.TsanAnalyzer):
1009  """ TsanAnalyzer that saves race reports to a file. """
1010
1011  TMP_FILE = "rvlog.tmp"
1012
1013  def __init__(self, source_dir, use_gdb):
1014    super(ThreadSanitizerRV1Analyzer, self).__init__(source_dir, use_gdb)
1015    self.out = open(self.TMP_FILE, "w")
1016
1017  def Report(self, files, testcase, check_sanity=False):
1018    reports = self.GetReports(files)
1019    for report in reports:
1020      print >>self.out, report
1021    if len(reports) > 0:
1022      logging.info("RaceVerifier pass 1 of 2, found %i reports" % len(reports))
1023      return -1
1024    return 0
1025
1026  def CloseOutputFile(self):
1027    self.out.close()
1028
1029
1030class ThreadSanitizerRV1Mixin(object):
1031  """RaceVerifier first pass.
1032
1033  Runs ThreadSanitizer as usual, but hides race reports and collects them in a
1034  temporary file"""
1035
1036  def __init__(self):
1037    super(ThreadSanitizerRV1Mixin, self).__init__()
1038    self.RegisterOptionParserHook(ThreadSanitizerRV1Mixin.ExtendOptionParser)
1039
1040  def ExtendOptionParser(self, parser):
1041    parser.set_defaults(hybrid="yes")
1042
1043  def CreateAnalyzer(self):
1044    use_gdb = common.IsMac()
1045    self.analyzer = ThreadSanitizerRV1Analyzer(self._source_dir, use_gdb)
1046    return self.analyzer
1047
1048  def Cleanup(self):
1049    super(ThreadSanitizerRV1Mixin, self).Cleanup()
1050    self.analyzer.CloseOutputFile()
1051
1052
1053class ThreadSanitizerRV2Mixin(object):
1054  """RaceVerifier second pass."""
1055
1056  def __init__(self):
1057    super(ThreadSanitizerRV2Mixin, self).__init__()
1058    self.RegisterOptionParserHook(ThreadSanitizerRV2Mixin.ExtendOptionParser)
1059
1060  def ExtendOptionParser(self, parser):
1061    parser.add_option("", "--race-verifier-sleep-ms",
1062                            dest="race_verifier_sleep_ms", default=10,
1063                            help="duration of RaceVerifier delays")
1064
1065  def ToolSpecificFlags(self):
1066    proc = super(ThreadSanitizerRV2Mixin, self).ToolSpecificFlags()
1067    proc += ['--race-verifier=%s' % ThreadSanitizerRV1Analyzer.TMP_FILE,
1068             '--race-verifier-sleep-ms=%d' %
1069             int(self._options.race_verifier_sleep_ms)]
1070    return proc
1071
1072  def Cleanup(self):
1073    super(ThreadSanitizerRV2Mixin, self).Cleanup()
1074    os.unlink(ThreadSanitizerRV1Analyzer.TMP_FILE)
1075
1076
1077class ThreadSanitizerRV1Posix(ThreadSanitizerRV1Mixin, ThreadSanitizerPosix):
1078  pass
1079
1080
1081class ThreadSanitizerRV2Posix(ThreadSanitizerRV2Mixin, ThreadSanitizerPosix):
1082  pass
1083
1084
1085class ThreadSanitizerRV1Windows(ThreadSanitizerRV1Mixin,
1086                                ThreadSanitizerWindows):
1087  pass
1088
1089
1090class ThreadSanitizerRV2Windows(ThreadSanitizerRV2Mixin,
1091                                ThreadSanitizerWindows):
1092  pass
1093
1094
1095class RaceVerifier(object):
1096  """Runs tests under RaceVerifier/Valgrind."""
1097
1098  MORE_INFO_URL = "http://code.google.com/p/data-race-test/wiki/RaceVerifier"
1099
1100  def RV1Factory(self):
1101    if common.IsWindows():
1102      return ThreadSanitizerRV1Windows()
1103    else:
1104      return ThreadSanitizerRV1Posix()
1105
1106  def RV2Factory(self):
1107    if common.IsWindows():
1108      return ThreadSanitizerRV2Windows()
1109    else:
1110      return ThreadSanitizerRV2Posix()
1111
1112  def ToolName(self):
1113    return "tsan"
1114
1115  def Main(self, args, check_sanity, min_runtime_in_seconds):
1116    logging.info("Running a TSan + RaceVerifier test. For more information, " +
1117                 "see " + self.MORE_INFO_URL)
1118    cmd1 = self.RV1Factory()
1119    ret = cmd1.Main(args, check_sanity, min_runtime_in_seconds)
1120    # Verify race reports, if there are any.
1121    if ret == -1:
1122      logging.info("Starting pass 2 of 2. Running the same binary in " +
1123                   "RaceVerifier mode to confirm possible race reports.")
1124      logging.info("For more information, see " + self.MORE_INFO_URL)
1125      cmd2 = self.RV2Factory()
1126      ret = cmd2.Main(args, check_sanity, min_runtime_in_seconds)
1127    else:
1128      logging.info("No reports, skipping RaceVerifier second pass")
1129    logging.info("Please see " + self.MORE_INFO_URL + " for more information " +
1130                 "on RaceVerifier")
1131    return ret
1132
1133  def Run(self, args, module, min_runtime_in_seconds=0):
1134   return self.Main(args, False, min_runtime_in_seconds)
1135
1136
1137class EmbeddedTool(BaseTool):
1138  """Abstract class for tools embedded directly into the test binary.
1139  """
1140  # TODO(glider): need to override Execute() and support process chaining here.
1141
1142  def ToolCommand(self):
1143    # In the simplest case just the args of the script.
1144    return self._args
1145
1146
1147class Asan(EmbeddedTool):
1148  """AddressSanitizer, a memory error detector.
1149
1150  More information at
1151  http://dev.chromium.org/developers/testing/addresssanitizer
1152  """
1153  def __init__(self):
1154    super(Asan, self).__init__()
1155    self._timeout = 1200
1156    if common.IsMac():
1157      self._env["DYLD_NO_PIE"] = "1"
1158
1159
1160  def ToolName(self):
1161    return "asan"
1162
1163  def ToolCommand(self):
1164    # TODO(glider): use pipes instead of the ugly wrapper here once they
1165    # are supported.
1166    procs = [os.path.join(self._source_dir, "tools", "valgrind",
1167                              "asan", "asan_wrapper.sh")]
1168    procs.extend(self._args)
1169    return procs
1170
1171  def Analyze(sels, unused_check_sanity):
1172    return 0
1173
1174
1175class TsanGcc(EmbeddedTool):
1176  """ThreadSanitizer with compile-time instrumentation done using GCC.
1177
1178  More information at
1179  code.google.com/p/data-race-test/wiki/GccInstrumentation
1180  """
1181  def __init__(self):
1182    super(TsanGcc, self).__init__()
1183    self.RegisterOptionParserHook(TsanGcc.ExtendOptionParser)
1184
1185  def ExtendOptionParser(self, parser):
1186    parser.add_option("", "--suppressions", default=[],
1187                      action="append",
1188                      help="path to TSan suppression file")
1189
1190  def Setup(self, args):
1191    if not super(TsanGcc, self).Setup(args):
1192      return False
1193    ld_library_paths = []
1194    for tail in "lib32", "lib64":
1195      ld_library_paths.append(
1196          os.path.join(self._source_dir, "third_party",
1197                       "compiler-tsan", "gcc-current", tail))
1198    # LD_LIBRARY_PATH will be overriden.
1199    self._env["LD_LIBRARY_PATH"] = ":".join(ld_library_paths)
1200
1201    # TODO(glider): this is a temporary solution until Analyze is implemented.
1202    env_options = ["--error-exitcode=1"]
1203    # TODO(glider): merge this with other TSan suppressions code.
1204    suppression_count = 0
1205    for suppression_file in self._options.suppressions:
1206      if os.path.exists(suppression_file):
1207        suppression_count += 1
1208        env_options += ["--suppressions=%s" % suppression_file]
1209    if not suppression_count:
1210      logging.warning("WARNING: NOT USING SUPPRESSIONS!")
1211
1212    self._env["TSAN_ARGS"] = " ".join(env_options)
1213    return True
1214
1215  def ToolName(self):
1216    return "tsan"
1217
1218  def Analyze(self, unused_check_sanity):
1219    # TODO(glider): this should use tsan_analyze.TsanAnalyzer. As a temporary
1220    # solution we set the exit code to 1 when a report occurs, because TSan-GCC
1221    # does not support the --log-file flag yet.
1222    return 0
1223
1224
1225class ToolFactory:
1226  def Create(self, tool_name):
1227    if tool_name == "memcheck":
1228      return Memcheck()
1229    if tool_name == "tsan":
1230      if common.IsWindows():
1231        return ThreadSanitizerWindows()
1232      else:
1233        return ThreadSanitizerPosix()
1234    if tool_name == "drmemory" or tool_name == "drmemory_light":
1235      # TODO(timurrrr): remove support for "drmemory" when buildbots are
1236      # switched to drmemory_light OR make drmemory==drmemory_full the default
1237      # mode when the tool is mature enough.
1238      return DrMemory(False, False)
1239    if tool_name == "drmemory_full":
1240      return DrMemory(True, False)
1241    if tool_name == "drmemory_pattern":
1242      return DrMemory(False, True)
1243    if tool_name == "tsan_rv":
1244      return RaceVerifier()
1245    if tool_name == "tsan_gcc":
1246      return TsanGcc()
1247    if tool_name == "asan":
1248      return Asan()
1249    try:
1250      platform_name = common.PlatformNames()[0]
1251    except common.NotImplementedError:
1252      platform_name = sys.platform + "(Unknown)"
1253    raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name,
1254                                                                 platform_name)
1255
1256def CreateTool(tool):
1257  return ToolFactory().Create(tool)
1258