Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2012 the V8 project authors. All rights reserved.
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 #
      8 #     * Redistributions of source code must retain the above copyright
      9 #       notice, this list of conditions and the following disclaimer.
     10 #     * Redistributions in binary form must reproduce the above
     11 #       copyright notice, this list of conditions and the following
     12 #       disclaimer in the documentation and/or other materials provided
     13 #       with the distribution.
     14 #     * Neither the name of Google Inc. nor the names of its
     15 #       contributors may be used to endorse or promote products derived
     16 #       from this software without specific prior written permission.
     17 #
     18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 
     31 import json
     32 import math
     33 import multiprocessing
     34 import optparse
     35 import os
     36 from os.path import join
     37 import random
     38 import shlex
     39 import subprocess
     40 import sys
     41 import time
     42 
     43 from testrunner.local import execution
     44 from testrunner.local import progress
     45 from testrunner.local import testsuite
     46 from testrunner.local import utils
     47 from testrunner.local import verbose
     48 from testrunner.objects import context
     49 
     50 
     51 ARCH_GUESS = utils.DefaultArch()
     52 DEFAULT_TESTS = ["mjsunit", "webkit"]
     53 TIMEOUT_DEFAULT = 60
     54 TIMEOUT_SCALEFACTOR = {"debug"   : 4,
     55                        "release" : 1 }
     56 
     57 MODE_FLAGS = {
     58     "debug"   : ["--nobreak-on-abort", "--nodead-code-elimination",
     59                  "--nofold-constants", "--enable-slow-asserts",
     60                  "--debug-code", "--verify-heap",
     61                  "--noconcurrent-recompilation"],
     62     "release" : ["--nobreak-on-abort", "--nodead-code-elimination",
     63                  "--nofold-constants", "--noconcurrent-recompilation"]}
     64 
     65 SUPPORTED_ARCHS = ["android_arm",
     66                    "android_ia32",
     67                    "arm",
     68                    "ia32",
     69                    "mipsel",
     70                    "nacl_ia32",
     71                    "nacl_x64",
     72                    "x64"]
     73 # Double the timeout for these:
     74 SLOW_ARCHS = ["android_arm",
     75               "android_ia32",
     76               "arm",
     77               "mipsel",
     78               "nacl_ia32",
     79               "nacl_x64"]
     80 MAX_DEOPT = 1000000000
     81 DISTRIBUTION_MODES = ["smooth", "random"]
     82 
     83 
     84 class RandomDistribution:
     85   def __init__(self, seed=None):
     86     seed = seed or random.randint(1, sys.maxint)
     87     print "Using random distribution with seed %d" % seed
     88     self._random = random.Random(seed)
     89 
     90   def Distribute(self, n, m):
     91     if n > m:
     92       n = m
     93     return self._random.sample(xrange(1, m + 1), n)
     94 
     95 
     96 class SmoothDistribution:
     97   """Distribute n numbers into the interval [1:m].
     98   F1: Factor of the first derivation of the distribution function.
     99   F2: Factor of the second derivation of the distribution function.
    100   With F1 and F2 set to 0, the distribution will be equal.
    101   """
    102   def __init__(self, factor1=2.0, factor2=0.2):
    103     self._factor1 = factor1
    104     self._factor2 = factor2
    105 
    106   def Distribute(self, n, m):
    107     if n > m:
    108       n = m
    109     if n <= 1:
    110       return [ 1 ]
    111 
    112     result = []
    113     x = 0.0
    114     dx = 1.0
    115     ddx = self._factor1
    116     dddx = self._factor2
    117     for i in range(0, n):
    118       result += [ x ]
    119       x += dx
    120       dx += ddx
    121       ddx += dddx
    122 
    123     # Project the distribution into the interval [0:M].
    124     result = [ x * m / result[-1] for x in result ]
    125 
    126     # Equalize by n. The closer n is to m, the more equal will be the
    127     # distribution.
    128     for (i, x) in enumerate(result):
    129       # The value of x if it was equally distributed.
    130       equal_x = i / float(n - 1) * float(m - 1) + 1
    131 
    132       # Difference factor between actual and equal distribution.
    133       diff = 1 - (x / equal_x)
    134 
    135       # Equalize x dependent on the number of values to distribute.
    136       result[i] = int(x + (i + 1) * diff)
    137     return result
    138 
    139 
    140 def Distribution(options):
    141   if options.distribution_mode == "random":
    142     return RandomDistribution(options.seed)
    143   if options.distribution_mode == "smooth":
    144     return SmoothDistribution(options.distribution_factor1,
    145                               options.distribution_factor2)
    146 
    147 
    148 def BuildOptions():
    149   result = optparse.OptionParser()
    150   result.add_option("--arch",
    151                     help=("The architecture to run tests for, "
    152                           "'auto' or 'native' for auto-detect"),
    153                     default="ia32,x64,arm")
    154   result.add_option("--arch-and-mode",
    155                     help="Architecture and mode in the format 'arch.mode'",
    156                     default=None)
    157   result.add_option("--buildbot",
    158                     help="Adapt to path structure used on buildbots",
    159                     default=False, action="store_true")
    160   result.add_option("--command-prefix",
    161                     help="Prepended to each shell command used to run a test",
    162                     default="")
    163   result.add_option("--coverage", help=("Exponential test coverage "
    164                     "(range 0.0, 1.0) -- 0.0: one test, 1.0 all tests (slow)"),
    165                     default=0.4, type="float")
    166   result.add_option("--coverage-lift", help=("Lifts test coverage for tests "
    167                     "with a small number of deopt points (range 0, inf)"),
    168                     default=20, type="int")
    169   result.add_option("--download-data", help="Download missing test suite data",
    170                     default=False, action="store_true")
    171   result.add_option("--distribution-factor1", help=("Factor of the first "
    172                     "derivation of the distribution function"), default=2.0,
    173                     type="float")
    174   result.add_option("--distribution-factor2", help=("Factor of the second "
    175                     "derivation of the distribution function"), default=0.7,
    176                     type="float")
    177   result.add_option("--distribution-mode", help=("How to select deopt points "
    178                     "for a given test (smooth|random)"),
    179                     default="smooth")
    180   result.add_option("--dump-results-file", help=("Dump maximum number of "
    181                     "deopt points per test to a file"))
    182   result.add_option("--extra-flags",
    183                     help="Additional flags to pass to each test command",
    184                     default="")
    185   result.add_option("--isolates", help="Whether to test isolates",
    186                     default=False, action="store_true")
    187   result.add_option("-j", help="The number of parallel tasks to run",
    188                     default=0, type="int")
    189   result.add_option("-m", "--mode",
    190                     help="The test modes in which to run (comma-separated)",
    191                     default="release,debug")
    192   result.add_option("--outdir", help="Base directory with compile output",
    193                     default="out")
    194   result.add_option("-p", "--progress",
    195                     help=("The style of progress indicator"
    196                           " (verbose, dots, color, mono)"),
    197                     choices=progress.PROGRESS_INDICATORS.keys(),
    198                     default="mono")
    199   result.add_option("--shard-count",
    200                     help="Split testsuites into this number of shards",
    201                     default=1, type="int")
    202   result.add_option("--shard-run",
    203                     help="Run this shard from the split up tests.",
    204                     default=1, type="int")
    205   result.add_option("--shell-dir", help="Directory containing executables",
    206                     default="")
    207   result.add_option("--seed", help="The seed for the random distribution",
    208                     type="int")
    209   result.add_option("-t", "--timeout", help="Timeout in seconds",
    210                     default= -1, type="int")
    211   result.add_option("-v", "--verbose", help="Verbose output",
    212                     default=False, action="store_true")
    213   return result
    214 
    215 
    216 def ProcessOptions(options):
    217   global VARIANT_FLAGS
    218 
    219   # Architecture and mode related stuff.
    220   if options.arch_and_mode:
    221     tokens = options.arch_and_mode.split(".")
    222     options.arch = tokens[0]
    223     options.mode = tokens[1]
    224   options.mode = options.mode.split(",")
    225   for mode in options.mode:
    226     if not mode.lower() in ["debug", "release"]:
    227       print "Unknown mode %s" % mode
    228       return False
    229   if options.arch in ["auto", "native"]:
    230     options.arch = ARCH_GUESS
    231   options.arch = options.arch.split(",")
    232   for arch in options.arch:
    233     if not arch in SUPPORTED_ARCHS:
    234       print "Unknown architecture %s" % arch
    235       return False
    236 
    237   # Special processing of other options, sorted alphabetically.
    238   options.command_prefix = shlex.split(options.command_prefix)
    239   options.extra_flags = shlex.split(options.extra_flags)
    240   if options.j == 0:
    241     options.j = multiprocessing.cpu_count()
    242   if not options.distribution_mode in DISTRIBUTION_MODES:
    243     print "Unknown distribution mode %s" % options.distribution_mode
    244     return False
    245   if options.distribution_factor1 < 0.0:
    246     print ("Distribution factor1 %s is out of range. Defaulting to 0.0"
    247         % options.distribution_factor1)
    248     options.distribution_factor1 = 0.0
    249   if options.distribution_factor2 < 0.0:
    250     print ("Distribution factor2 %s is out of range. Defaulting to 0.0"
    251         % options.distribution_factor2)
    252     options.distribution_factor2 = 0.0
    253   if options.coverage < 0.0 or options.coverage > 1.0:
    254     print ("Coverage %s is out of range. Defaulting to 0.4"
    255         % options.coverage)
    256     options.coverage = 0.4
    257   if options.coverage_lift < 0:
    258     print ("Coverage lift %s is out of range. Defaulting to 0"
    259         % options.coverage_lift)
    260     options.coverage_lift = 0
    261   return True
    262 
    263 
    264 def ShardTests(tests, shard_count, shard_run):
    265   if shard_count < 2:
    266     return tests
    267   if shard_run < 1 or shard_run > shard_count:
    268     print "shard-run not a valid number, should be in [1:shard-count]"
    269     print "defaulting back to running all tests"
    270     return tests
    271   count = 0
    272   shard = []
    273   for test in tests:
    274     if count % shard_count == shard_run - 1:
    275       shard.append(test)
    276     count += 1
    277   return shard
    278 
    279 
    280 def Main():
    281   parser = BuildOptions()
    282   (options, args) = parser.parse_args()
    283   if not ProcessOptions(options):
    284     parser.print_help()
    285     return 1
    286 
    287   exit_code = 0
    288   workspace = os.path.abspath(join(os.path.dirname(sys.argv[0]), ".."))
    289 
    290   suite_paths = utils.GetSuitePaths(join(workspace, "test"))
    291 
    292   if len(args) == 0:
    293     suite_paths = [ s for s in suite_paths if s in DEFAULT_TESTS ]
    294   else:
    295     args_suites = set()
    296     for arg in args:
    297       suite = arg.split(os.path.sep)[0]
    298       if not suite in args_suites:
    299         args_suites.add(suite)
    300     suite_paths = [ s for s in suite_paths if s in args_suites ]
    301 
    302   suites = []
    303   for root in suite_paths:
    304     suite = testsuite.TestSuite.LoadTestSuite(
    305         os.path.join(workspace, "test", root))
    306     if suite:
    307       suites.append(suite)
    308 
    309   if options.download_data:
    310     for s in suites:
    311       s.DownloadData()
    312 
    313   for mode in options.mode:
    314     for arch in options.arch:
    315       code = Execute(arch, mode, args, options, suites, workspace)
    316       exit_code = exit_code or code
    317   return exit_code
    318 
    319 
    320 def CalculateNTests(m, options):
    321   """Calculates the number of tests from m deopt points with exponential
    322   coverage.
    323   The coverage is expected to be between 0.0 and 1.0.
    324   The 'coverage lift' lifts the coverage for tests with smaller m values.
    325   """
    326   c = float(options.coverage)
    327   l = float(options.coverage_lift)
    328   return int(math.pow(m, (m * c + l) / (m + l)))
    329 
    330 
    331 def Execute(arch, mode, args, options, suites, workspace):
    332   print(">>> Running tests for %s.%s" % (arch, mode))
    333 
    334   dist = Distribution(options)
    335 
    336   shell_dir = options.shell_dir
    337   if not shell_dir:
    338     if options.buildbot:
    339       shell_dir = os.path.join(workspace, options.outdir, mode)
    340       mode = mode.lower()
    341     else:
    342       shell_dir = os.path.join(workspace, options.outdir,
    343                                "%s.%s" % (arch, mode))
    344   shell_dir = os.path.relpath(shell_dir)
    345 
    346   # Populate context object.
    347   mode_flags = MODE_FLAGS[mode]
    348   timeout = options.timeout
    349   if timeout == -1:
    350     # Simulators are slow, therefore allow a longer default timeout.
    351     if arch in SLOW_ARCHS:
    352       timeout = 2 * TIMEOUT_DEFAULT;
    353     else:
    354       timeout = TIMEOUT_DEFAULT;
    355 
    356   timeout *= TIMEOUT_SCALEFACTOR[mode]
    357   ctx = context.Context(arch, mode, shell_dir,
    358                         mode_flags, options.verbose,
    359                         timeout, options.isolates,
    360                         options.command_prefix,
    361                         options.extra_flags,
    362                         False)
    363 
    364   # Find available test suites and read test cases from them.
    365   variables = {
    366     "mode": mode,
    367     "arch": arch,
    368     "system": utils.GuessOS(),
    369     "isolates": options.isolates,
    370     "deopt_fuzzer": True,
    371     "no_i18n": False,
    372   }
    373   all_tests = []
    374   num_tests = 0
    375   test_id = 0
    376 
    377   # Remember test case prototypes for the fuzzing phase.
    378   test_backup = dict((s, []) for s in suites)
    379 
    380   for s in suites:
    381     s.ReadStatusFile(variables)
    382     s.ReadTestCases(ctx)
    383     if len(args) > 0:
    384       s.FilterTestCasesByArgs(args)
    385     all_tests += s.tests
    386     s.FilterTestCasesByStatus(False)
    387     test_backup[s] = s.tests
    388     analysis_flags = ["--deopt-every-n-times", "%d" % MAX_DEOPT,
    389                       "--print-deopt-stress"]
    390     s.tests = [ t.CopyAddingFlags(analysis_flags) for t in s.tests ]
    391     num_tests += len(s.tests)
    392     for t in s.tests:
    393       t.id = test_id
    394       test_id += 1
    395 
    396   if num_tests == 0:
    397     print "No tests to run."
    398     return 0
    399 
    400   try:
    401     print(">>> Collection phase")
    402     progress_indicator = progress.PROGRESS_INDICATORS[options.progress]()
    403     runner = execution.Runner(suites, progress_indicator, ctx)
    404 
    405     exit_code = runner.Run(options.j)
    406     if runner.terminate:
    407       return exit_code
    408 
    409   except KeyboardInterrupt:
    410     return 1
    411 
    412   print(">>> Analysis phase")
    413   num_tests = 0
    414   test_id = 0
    415   for s in suites:
    416     test_results = {}
    417     for t in s.tests:
    418       for line in t.output.stdout.splitlines():
    419         if line.startswith("=== Stress deopt counter: "):
    420           test_results[t.path] = MAX_DEOPT - int(line.split(" ")[-1])
    421     for t in s.tests:
    422       if t.path not in test_results:
    423         print "Missing results for %s" % t.path
    424     if options.dump_results_file:
    425       results_dict = dict((t.path, n) for (t, n) in test_results.iteritems())
    426       with file("%s.%d.txt" % (dump_results_file, time.time()), "w") as f:
    427         f.write(json.dumps(results_dict))
    428 
    429     # Reset tests and redistribute the prototypes from the collection phase.
    430     s.tests = []
    431     if options.verbose:
    432       print "Test distributions:"
    433     for t in test_backup[s]:
    434       max_deopt = test_results.get(t.path, 0)
    435       if max_deopt == 0:
    436         continue
    437       n_deopt = CalculateNTests(max_deopt, options)
    438       distribution = dist.Distribute(n_deopt, max_deopt)
    439       if options.verbose:
    440         print "%s %s" % (t.path, distribution)
    441       for i in distribution:
    442         fuzzing_flags = ["--deopt-every-n-times", "%d" % i]
    443         s.tests.append(t.CopyAddingFlags(fuzzing_flags))
    444     num_tests += len(s.tests)
    445     for t in s.tests:
    446       t.id = test_id
    447       test_id += 1
    448 
    449   if num_tests == 0:
    450     print "No tests to run."
    451     return 0
    452 
    453   try:
    454     print(">>> Deopt fuzzing phase (%d test cases)" % num_tests)
    455     progress_indicator = progress.PROGRESS_INDICATORS[options.progress]()
    456     runner = execution.Runner(suites, progress_indicator, ctx)
    457 
    458     exit_code = runner.Run(options.j)
    459     if runner.terminate:
    460       return exit_code
    461 
    462   except KeyboardInterrupt:
    463     return 1
    464 
    465   return exit_code
    466 
    467 
    468 if __name__ == "__main__":
    469   sys.exit(Main())
    470