Home | History | Annotate | Download | only in scripts
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Runs an exe through Valgrind and puts the intermediate files in a
      6 directory.
      7 """
      8 
      9 import datetime
     10 import glob
     11 import logging
     12 import optparse
     13 import os
     14 import re
     15 import shutil
     16 import stat
     17 import subprocess
     18 import sys
     19 import tempfile
     20 
     21 import common
     22 
     23 import drmemory_analyze
     24 
     25 class BaseTool(object):
     26   """Abstract class for running dynamic error detection tools.
     27 
     28   Always subclass this and implement ToolCommand with framework- and
     29   tool-specific stuff.
     30   """
     31 
     32   def __init__(self):
     33     temp_parent_dir = None
     34     self.log_parent_dir = ""
     35     if common.IsWindows():
     36       # gpu process on Windows Vista+ runs at Low Integrity and can only
     37       # write to certain directories (http://crbug.com/119131)
     38       #
     39       # TODO(bruening): if scripts die in middle and don't clean up temp
     40       # dir, we'll accumulate files in profile dir.  should remove
     41       # really old files automatically.
     42       profile = os.getenv("USERPROFILE")
     43       if profile:
     44         self.log_parent_dir = profile + "\\AppData\\LocalLow\\"
     45         if os.path.exists(self.log_parent_dir):
     46           self.log_parent_dir = common.NormalizeWindowsPath(self.log_parent_dir)
     47           temp_parent_dir = self.log_parent_dir
     48     # Generated every time (even when overridden)
     49     self.temp_dir = tempfile.mkdtemp(prefix="vg_logs_", dir=temp_parent_dir)
     50     self.log_dir = self.temp_dir # overridable by --keep_logs
     51     self.option_parser_hooks = []
     52     # TODO(glider): we may not need some of the env vars on some of the
     53     # platforms.
     54     self._env = {
     55       "G_SLICE" : "always-malloc",
     56       "NSS_DISABLE_UNLOAD" : "1",
     57       "NSS_DISABLE_ARENA_FREE_LIST" : "1",
     58       "GTEST_DEATH_TEST_USE_FORK": "1",
     59     }
     60 
     61   def ToolName(self):
     62     raise NotImplementedError, "This method should be implemented " \
     63                                "in the tool-specific subclass"
     64 
     65   def Analyze(self, check_sanity=False):
     66     raise NotImplementedError, "This method should be implemented " \
     67                                "in the tool-specific subclass"
     68 
     69   def RegisterOptionParserHook(self, hook):
     70     # Frameworks and tools can add their own flags to the parser.
     71     self.option_parser_hooks.append(hook)
     72 
     73   def CreateOptionParser(self):
     74     # Defines Chromium-specific flags.
     75     self._parser = optparse.OptionParser("usage: %prog [options] <program to "
     76                                          "test>")
     77     self._parser.disable_interspersed_args()
     78     self._parser.add_option("-t", "--timeout",
     79                       dest="timeout", metavar="TIMEOUT", default=100000,
     80                       help="timeout in seconds for the run (default 100000)")
     81     self._parser.add_option("", "--build-dir",
     82                             help="the location of the compiler output")
     83     self._parser.add_option("", "--source-dir",
     84                             help="path to top of source tree for this build"
     85                                  "(used to normalize source paths in baseline)")
     86     self._parser.add_option("", "--gtest_filter", default="",
     87                             help="which test case to run")
     88     self._parser.add_option("", "--gtest_repeat",
     89                             help="how many times to run each test")
     90     self._parser.add_option("", "--gtest_print_time", action="store_true",
     91                             default=False,
     92                             help="show how long each test takes")
     93     self._parser.add_option("", "--ignore_exit_code", action="store_true",
     94                             default=False,
     95                             help="ignore exit code of the test "
     96                                  "(e.g. test failures)")
     97     self._parser.add_option("", "--keep_logs", action="store_true",
     98                             default=False,
     99                             help="store memory tool logs in the <tool>.logs "
    100                                  "directory instead of /tmp.\nThis can be "
    101                                  "useful for tool developers/maintainers.\n"
    102                                  "Please note that the <tool>.logs directory "
    103                                  "will be clobbered on tool startup.")
    104 
    105     # To add framework- or tool-specific flags, please add a hook using
    106     # RegisterOptionParserHook in the corresponding subclass.
    107     # See ValgrindTool for an example.
    108     for hook in self.option_parser_hooks:
    109       hook(self, self._parser)
    110 
    111   def ParseArgv(self, args):
    112     self.CreateOptionParser()
    113 
    114     # self._tool_flags will store those tool flags which we don't parse
    115     # manually in this script.
    116     self._tool_flags = []
    117     known_args = []
    118 
    119     """ We assume that the first argument not starting with "-" is a program
    120     name and all the following flags should be passed to the program.
    121     TODO(timurrrr): customize optparse instead
    122     """
    123     while len(args) > 0 and args[0][:1] == "-":
    124       arg = args[0]
    125       if (arg == "--"):
    126         break
    127       if self._parser.has_option(arg.split("=")[0]):
    128         known_args += [arg]
    129       else:
    130         self._tool_flags += [arg]
    131       args = args[1:]
    132 
    133     if len(args) > 0:
    134       known_args += args
    135 
    136     self._options, self._args = self._parser.parse_args(known_args)
    137 
    138     self._timeout = int(self._options.timeout)
    139     self._source_dir = self._options.source_dir
    140     if self._options.keep_logs:
    141       # log_parent_dir has trailing slash if non-empty
    142       self.log_dir = self.log_parent_dir + "%s.logs" % self.ToolName()
    143       if os.path.exists(self.log_dir):
    144         shutil.rmtree(self.log_dir)
    145       os.mkdir(self.log_dir)
    146       logging.info("Logs are in " + self.log_dir)
    147 
    148     self._ignore_exit_code = self._options.ignore_exit_code
    149     if self._options.gtest_filter != "":
    150       self._args.append("--gtest_filter=%s" % self._options.gtest_filter)
    151     if self._options.gtest_repeat:
    152       self._args.append("--gtest_repeat=%s" % self._options.gtest_repeat)
    153     if self._options.gtest_print_time:
    154       self._args.append("--gtest_print_time")
    155 
    156     return True
    157 
    158   def Setup(self, args):
    159     return self.ParseArgv(args)
    160 
    161   def ToolCommand(self):
    162     raise NotImplementedError, "This method should be implemented " \
    163                                "in the tool-specific subclass"
    164 
    165   def Cleanup(self):
    166     # You may override it in the tool-specific subclass
    167     pass
    168 
    169   def Execute(self):
    170     """ Execute the app to be tested after successful instrumentation.
    171     Full execution command-line provided by subclassers via proc."""
    172     logging.info("starting execution...")
    173     proc = self.ToolCommand()
    174     for var in self._env:
    175       common.PutEnvAndLog(var, self._env[var])
    176     return common.RunSubprocess(proc, self._timeout)
    177 
    178   def RunTestsAndAnalyze(self, check_sanity):
    179     exec_retcode = self.Execute()
    180     analyze_retcode = self.Analyze(check_sanity)
    181 
    182     if analyze_retcode:
    183       logging.error("Analyze failed.")
    184       logging.info("Search the log for '[ERROR]' to see the error reports.")
    185       return analyze_retcode
    186 
    187     if exec_retcode:
    188       if self._ignore_exit_code:
    189         logging.info("Test execution failed, but the exit code is ignored.")
    190       else:
    191         logging.error("Test execution failed.")
    192         return exec_retcode
    193     else:
    194       logging.info("Test execution completed successfully.")
    195 
    196     if not analyze_retcode:
    197       logging.info("Analysis completed successfully.")
    198 
    199     return 0
    200 
    201   def Main(self, args, check_sanity, min_runtime_in_seconds):
    202     """Call this to run through the whole process: Setup, Execute, Analyze"""
    203     start_time = datetime.datetime.now()
    204     retcode = -1
    205     if self.Setup(args):
    206       retcode = self.RunTestsAndAnalyze(check_sanity)
    207       shutil.rmtree(self.temp_dir, ignore_errors=True)
    208       self.Cleanup()
    209     else:
    210       logging.error("Setup failed")
    211     end_time = datetime.datetime.now()
    212     runtime_in_seconds = (end_time - start_time).seconds
    213     hours = runtime_in_seconds / 3600
    214     seconds = runtime_in_seconds % 3600
    215     minutes = seconds / 60
    216     seconds = seconds % 60
    217     logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds))
    218     if (min_runtime_in_seconds > 0 and
    219         runtime_in_seconds < min_runtime_in_seconds):
    220       logging.error("Layout tests finished too quickly. "
    221                     "It should have taken at least %d seconds. "
    222                     "Something went wrong?" % min_runtime_in_seconds)
    223       retcode = -1
    224     return retcode
    225 
    226   def Run(self, args, module, min_runtime_in_seconds=0):
    227     MODULES_TO_SANITY_CHECK = ["base"]
    228 
    229     check_sanity = module in MODULES_TO_SANITY_CHECK
    230     return self.Main(args, check_sanity, min_runtime_in_seconds)
    231 
    232 
    233 class DrMemory(BaseTool):
    234   """Dr.Memory
    235   Dynamic memory error detector for Windows.
    236 
    237   http://dev.chromium.org/developers/how-tos/using-drmemory
    238   It is not very mature at the moment, some things might not work properly.
    239   """
    240 
    241   def __init__(self, full_mode, pattern_mode):
    242     super(DrMemory, self).__init__()
    243     self.full_mode = full_mode
    244     self.pattern_mode = pattern_mode
    245     self.RegisterOptionParserHook(DrMemory.ExtendOptionParser)
    246 
    247   def ToolName(self):
    248     return "drmemory"
    249 
    250   def ExtendOptionParser(self, parser):
    251     parser.add_option("", "--suppressions", default=[],
    252                       action="append",
    253                       help="path to a drmemory suppression file")
    254     parser.add_option("", "--follow_python", action="store_true",
    255                       default=False, dest="follow_python",
    256                       help="Monitor python child processes.  If off, neither "
    257                       "python children nor any children of python children "
    258                       "will be monitored.")
    259     parser.add_option("", "--indirect_pdfium_test", action="store_true",
    260                       default=False,
    261                       help="set --wrapper rather than running Dr. Memory "
    262                       "directly.")
    263     parser.add_option("", "--use_debug", action="store_true",
    264                       default=False, dest="use_debug",
    265                       help="Run Dr. Memory debug build")
    266     parser.add_option("", "--trace_children", action="store_true",
    267                             default=True,
    268                             help="TODO: default value differs from Valgrind")
    269 
    270   def ToolCommand(self):
    271     """Get the tool command to run."""
    272     # WINHEAP is what Dr. Memory supports as there are issues w/ both
    273     # jemalloc (https://github.com/DynamoRIO/drmemory/issues/320) and
    274     # tcmalloc (https://github.com/DynamoRIO/drmemory/issues/314)
    275     add_env = {
    276       "CHROME_ALLOCATOR" : "WINHEAP",
    277       "JSIMD_FORCEMMX"   : "1",  # https://github.com/DynamoRIO/drmemory/issues/540
    278     }
    279     for k,v in add_env.iteritems():
    280       logging.info("export %s=%s", k, v)
    281       os.putenv(k, v)
    282 
    283     drmem_cmd = os.getenv("DRMEMORY_COMMAND")
    284     if not drmem_cmd:
    285       raise RuntimeError, "Please set DRMEMORY_COMMAND environment variable " \
    286                           "with the path to drmemory.exe"
    287     proc = drmem_cmd.split(" ")
    288 
    289     # By default, don't run python (this will exclude python's children as well)
    290     # to reduce runtime.  We're not really interested in spending time finding
    291     # bugs in the python implementation.
    292     # With file-based config we must update the file every time, and
    293     # it will affect simultaneous drmem uses by this user.  While file-based
    294     # config has many advantages, here we may want this-instance-only
    295     # (https://github.com/DynamoRIO/drmemory/issues/334).
    296     drconfig_cmd = [ proc[0].replace("drmemory.exe", "drconfig.exe") ]
    297     drconfig_cmd += ["-quiet"] # suppress errors about no 64-bit libs
    298     run_drconfig = True
    299     if self._options.follow_python:
    300       logging.info("Following python children")
    301       # -unreg fails if not already registered so query for that first
    302       query_cmd = drconfig_cmd + ["-isreg", "python.exe"]
    303       query_proc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE,
    304                                     shell=True)
    305       (query_out, query_err) = query_proc.communicate()
    306       if re.search("exe not registered", query_out):
    307         run_drconfig = False # all set
    308       else:
    309         drconfig_cmd += ["-unreg", "python.exe"]
    310     else:
    311       logging.info("Excluding python children")
    312       drconfig_cmd += ["-reg", "python.exe", "-norun"]
    313     if run_drconfig:
    314       drconfig_retcode = common.RunSubprocess(drconfig_cmd, self._timeout)
    315       if drconfig_retcode:
    316         logging.error("Configuring whether to follow python children failed " \
    317                       "with %d.", drconfig_retcode)
    318         raise RuntimeError, "Configuring python children failed "
    319 
    320     suppression_count = 0
    321     supp_files = self._options.suppressions
    322     if self.full_mode:
    323       supp_files += [s.replace(".txt", "_full.txt") for s in supp_files]
    324     for suppression_file in supp_files:
    325       if os.path.exists(suppression_file):
    326         suppression_count += 1
    327         proc += ["-suppress", common.NormalizeWindowsPath(suppression_file)]
    328 
    329     if not suppression_count:
    330       logging.warning("WARNING: NOT USING SUPPRESSIONS!")
    331 
    332     # Un-comment to dump Dr.Memory events on error
    333     #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"]
    334 
    335     # Un-comment and comment next line to debug Dr.Memory
    336     #proc += ["-dr_ops", "-no_hide"]
    337     #proc += ["-dr_ops", "-msgbox_mask", "-dr_ops", "15"]
    338     #Proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "15"]
    339     # Ensure we see messages about Dr. Memory crashing!
    340     proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "12"]
    341 
    342     if self._options.use_debug:
    343       proc += ["-debug"]
    344 
    345     proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)]
    346 
    347     if self.log_parent_dir:
    348       # gpu process on Windows Vista+ runs at Low Integrity and can only
    349       # write to certain directories (http://crbug.com/119131)
    350       symcache_dir = os.path.join(self.log_parent_dir, "drmemory.symcache")
    351     elif self._options.build_dir:
    352       # The other case is only possible with -t cmdline.
    353       # Anyways, if we omit -symcache_dir the -logdir's value is used which
    354       # should be fine.
    355       symcache_dir = os.path.join(self._options.build_dir, "drmemory.symcache")
    356     if symcache_dir:
    357       if not os.path.exists(symcache_dir):
    358         try:
    359           os.mkdir(symcache_dir)
    360         except OSError:
    361           logging.warning("Can't create symcache dir?")
    362       if os.path.exists(symcache_dir):
    363         proc += ["-symcache_dir", common.NormalizeWindowsPath(symcache_dir)]
    364 
    365     # Use -no_summary to suppress DrMemory's summary and init-time
    366     # notifications.  We generate our own with drmemory_analyze.py.
    367     proc += ["-batch", "-no_summary"]
    368 
    369     # Un-comment to disable interleaved output.  Will also suppress error
    370     # messages normally printed to stderr.
    371     #proc += ["-quiet", "-no_results_to_stderr"]
    372 
    373     proc += ["-callstack_max_frames", "40"]
    374 
    375     # disable leak scan for now
    376     proc += ["-no_count_leaks", "-no_leak_scan"]
    377 
    378     # disable warnings about unaddressable prefetches
    379     proc += ["-no_check_prefetch"]
    380 
    381     # crbug.com/413215, no heap mismatch check for Windows release build binary
    382     if common.IsWindows() and "Release" in self._options.build_dir:
    383         proc += ["-no_check_delete_mismatch"]
    384 
    385     # make callstacks easier to read
    386     proc += ["-callstack_srcfile_prefix",
    387              "build\\src,chromium\\src,crt_build\\self_x86"]
    388     proc += ["-callstack_modname_hide",
    389              "*drmemory*,chrome.dll"]
    390 
    391     boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False)
    392     # TODO(timurrrr): In fact, we want "starting from .." instead of "below .."
    393     proc += ["-callstack_truncate_below", ",".join(boring_callers)]
    394 
    395     if self.pattern_mode:
    396       proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"]
    397     elif not self.full_mode:
    398       proc += ["-light"]
    399 
    400     proc += self._tool_flags
    401 
    402     # Dr.Memory requires -- to separate tool flags from the executable name.
    403     proc += ["--"]
    404 
    405     if self._options.indirect_pdfium_test:
    406       wrapper = " ".join(proc)
    407       logging.info("pdfium wrapper = " + wrapper)
    408       proc = self._args
    409       proc += ["--wrapper", wrapper]
    410       return proc
    411 
    412     # Note that self._args begins with the name of the exe to be run.
    413     self._args[0] = common.NormalizeWindowsPath(self._args[0])
    414     proc += self._args
    415     return proc
    416 
    417   def CreateBrowserWrapper(self, command):
    418     os.putenv("BROWSER_WRAPPER", command)
    419 
    420   def Analyze(self, check_sanity=False):
    421     # Use one analyzer for all the log files to avoid printing duplicate reports
    422     #
    423     # TODO(timurrrr): unify this with Valgrind and other tools when we have
    424     # https://github.com/DynamoRIO/drmemory/issues/684
    425     analyzer = drmemory_analyze.DrMemoryAnalyzer()
    426 
    427     ret = 0
    428     if not self._options.indirect_pdfium_test:
    429       filenames = glob.glob(self.log_dir + "/*/results.txt")
    430 
    431       ret = analyzer.Report(filenames, None, check_sanity)
    432     else:
    433       testcases = glob.glob(self.log_dir + "/testcase.*.logs")
    434       # If we have browser wrapper, the per-test logdirs are named as
    435       # "testcase.wrapper_PID.name".
    436       # Let's extract the list of wrapper_PIDs and name it ppids.
    437       # NOTE: ppids may contain '_', i.e. they are not ints!
    438       ppids = set([f.split(".")[-2] for f in testcases])
    439 
    440       for ppid in ppids:
    441         testcase_name = None
    442         try:
    443           f = open("%s/testcase.%s.name" % (self.log_dir, ppid))
    444           testcase_name = f.read().strip()
    445           f.close()
    446         except IOError:
    447           pass
    448         print "====================================================="
    449         print " Below is the report for drmemory wrapper PID=%s." % ppid
    450         if testcase_name:
    451           print " It was used while running the `%s` test." % testcase_name
    452         else:
    453           # TODO(timurrrr): hm, the PID line is suppressed on Windows...
    454           print " You can find the corresponding test"
    455           print " by searching the above log for 'PID=%s'" % ppid
    456         sys.stdout.flush()
    457         ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" %
    458                                    (self.log_dir, ppid))
    459         ret |= analyzer.Report(ppid_filenames, testcase_name, False)
    460         print "====================================================="
    461         sys.stdout.flush()
    462 
    463     logging.info("Please see http://dev.chromium.org/developers/how-tos/"
    464                  "using-drmemory for the info on Dr. Memory")
    465     return ret
    466 
    467 
    468 class ToolFactory:
    469   def Create(self, tool_name):
    470     if tool_name == "drmemory" or tool_name == "drmemory_light":
    471       # TODO(timurrrr): remove support for "drmemory" when buildbots are
    472       # switched to drmemory_light OR make drmemory==drmemory_full the default
    473       # mode when the tool is mature enough.
    474       return DrMemory(False, False)
    475     if tool_name == "drmemory_full":
    476       return DrMemory(True, False)
    477     if tool_name == "drmemory_pattern":
    478       return DrMemory(False, True)
    479     try:
    480       platform_name = common.PlatformNames()[0]
    481     except common.NotImplementedError:
    482       platform_name = sys.platform + "(Unknown)"
    483     raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name,
    484                                                                  platform_name)
    485 
    486 def CreateTool(tool):
    487   return ToolFactory().Create(tool)
    488