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 # Base dir of the v8 checkout to be used as cwd.
     52 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     53 
     54 ARCH_GUESS = utils.DefaultArch()
     55 DEFAULT_TESTS = ["mjsunit", "webkit"]
     56 TIMEOUT_DEFAULT = 60
     57 TIMEOUT_SCALEFACTOR = {"debug"   : 4,
     58                        "release" : 1 }
     59 
     60 MODE_FLAGS = {
     61     "debug"   : ["--nohard-abort", "--nodead-code-elimination",
     62                  "--nofold-constants", "--enable-slow-asserts",
     63                  "--debug-code", "--verify-heap",
     64                  "--noconcurrent-recompilation"],
     65     "release" : ["--nohard-abort", "--nodead-code-elimination",
     66                  "--nofold-constants", "--noconcurrent-recompilation"]}
     67 
     68 SUPPORTED_ARCHS = ["android_arm",
     69                    "android_ia32",
     70                    "arm",
     71                    "ia32",
     72                    "ppc",
     73                    "ppc64",
     74                    "s390",
     75                    "s390x",
     76                    "mipsel",
     77                    "x64"]
     78 # Double the timeout for these:
     79 SLOW_ARCHS = ["android_arm",
     80               "android_ia32",
     81               "arm",
     82               "mipsel"]
     83 MAX_DEOPT = 1000000000
     84 DISTRIBUTION_MODES = ["smooth", "random"]
     85 
     86 
     87 class RandomDistribution:
     88   def __init__(self, seed=None):
     89     seed = seed or random.randint(1, sys.maxint)
     90     print "Using random distribution with seed %d" % seed
     91     self._random = random.Random(seed)
     92 
     93   def Distribute(self, n, m):
     94     if n > m:
     95       n = m
     96     return self._random.sample(xrange(1, m + 1), n)
     97 
     98 
     99 class SmoothDistribution:
    100   """Distribute n numbers into the interval [1:m].
    101   F1: Factor of the first derivation of the distribution function.
    102   F2: Factor of the second derivation of the distribution function.
    103   With F1 and F2 set to 0, the distribution will be equal.
    104   """
    105   def __init__(self, factor1=2.0, factor2=0.2):
    106     self._factor1 = factor1
    107     self._factor2 = factor2
    108 
    109   def Distribute(self, n, m):
    110     if n > m:
    111       n = m
    112     if n <= 1:
    113       return [ 1 ]
    114 
    115     result = []
    116     x = 0.0
    117     dx = 1.0
    118     ddx = self._factor1
    119     dddx = self._factor2
    120     for i in range(0, n):
    121       result += [ x ]
    122       x += dx
    123       dx += ddx
    124       ddx += dddx
    125 
    126     # Project the distribution into the interval [0:M].
    127     result = [ x * m / result[-1] for x in result ]
    128 
    129     # Equalize by n. The closer n is to m, the more equal will be the
    130     # distribution.
    131     for (i, x) in enumerate(result):
    132       # The value of x if it was equally distributed.
    133       equal_x = i / float(n - 1) * float(m - 1) + 1
    134 
    135       # Difference factor between actual and equal distribution.
    136       diff = 1 - (x / equal_x)
    137 
    138       # Equalize x dependent on the number of values to distribute.
    139       result[i] = int(x + (i + 1) * diff)
    140     return result
    141 
    142 
    143 def Distribution(options):
    144   if options.distribution_mode == "random":
    145     return RandomDistribution(options.seed)
    146   if options.distribution_mode == "smooth":
    147     return SmoothDistribution(options.distribution_factor1,
    148                               options.distribution_factor2)
    149 
    150 
    151 def BuildOptions():
    152   result = optparse.OptionParser()
    153   result.add_option("--arch",
    154                     help=("The architecture to run tests for, "
    155                           "'auto' or 'native' for auto-detect"),
    156                     default="ia32,x64,arm")
    157   result.add_option("--arch-and-mode",
    158                     help="Architecture and mode in the format 'arch.mode'",
    159                     default=None)
    160   result.add_option("--asan",
    161                     help="Regard test expectations for ASAN",
    162                     default=False, action="store_true")
    163   result.add_option("--buildbot",
    164                     help="Adapt to path structure used on buildbots",
    165                     default=False, action="store_true")
    166   result.add_option("--dcheck-always-on",
    167                     help="Indicates that V8 was compiled with DCHECKs enabled",
    168                     default=False, action="store_true")
    169   result.add_option("--command-prefix",
    170                     help="Prepended to each shell command used to run a test",
    171                     default="")
    172   result.add_option("--coverage", help=("Exponential test coverage "
    173                     "(range 0.0, 1.0) -- 0.0: one test, 1.0 all tests (slow)"),
    174                     default=0.4, type="float")
    175   result.add_option("--coverage-lift", help=("Lifts test coverage for tests "
    176                     "with a small number of deopt points (range 0, inf)"),
    177                     default=20, type="int")
    178   result.add_option("--download-data", help="Download missing test suite data",
    179                     default=False, action="store_true")
    180   result.add_option("--distribution-factor1", help=("Factor of the first "
    181                     "derivation of the distribution function"), default=2.0,
    182                     type="float")
    183   result.add_option("--distribution-factor2", help=("Factor of the second "
    184                     "derivation of the distribution function"), default=0.7,
    185                     type="float")
    186   result.add_option("--distribution-mode", help=("How to select deopt points "
    187                     "for a given test (smooth|random)"),
    188                     default="smooth")
    189   result.add_option("--dump-results-file", help=("Dump maximum number of "
    190                     "deopt points per test to a file"))
    191   result.add_option("--extra-flags",
    192                     help="Additional flags to pass to each test command",
    193                     default="")
    194   result.add_option("--isolates", help="Whether to test isolates",
    195                     default=False, action="store_true")
    196   result.add_option("-j", help="The number of parallel tasks to run",
    197                     default=0, type="int")
    198   result.add_option("-m", "--mode",
    199                     help="The test modes in which to run (comma-separated)",
    200                     default="release,debug")
    201   result.add_option("--outdir", help="Base directory with compile output",
    202                     default="out")
    203   result.add_option("-p", "--progress",
    204                     help=("The style of progress indicator"
    205                           " (verbose, dots, color, mono)"),
    206                     choices=progress.PROGRESS_INDICATORS.keys(),
    207                     default="mono")
    208   result.add_option("--shard-count",
    209                     help="Split testsuites into this number of shards",
    210                     default=1, type="int")
    211   result.add_option("--shard-run",
    212                     help="Run this shard from the split up tests.",
    213                     default=1, type="int")
    214   result.add_option("--shell-dir", help="Directory containing executables",
    215                     default="")
    216   result.add_option("--seed", help="The seed for the random distribution",
    217                     type="int")
    218   result.add_option("-t", "--timeout", help="Timeout in seconds",
    219                     default= -1, type="int")
    220   result.add_option("-v", "--verbose", help="Verbose output",
    221                     default=False, action="store_true")
    222   result.add_option("--random-seed", default=0, dest="random_seed",
    223                     help="Default seed for initializing random generator")
    224   return result
    225 
    226 
    227 def ProcessOptions(options):
    228   global VARIANT_FLAGS
    229 
    230   # Architecture and mode related stuff.
    231   if options.arch_and_mode:
    232     tokens = options.arch_and_mode.split(".")
    233     options.arch = tokens[0]
    234     options.mode = tokens[1]
    235   options.mode = options.mode.split(",")
    236   for mode in options.mode:
    237     if not mode.lower() in ["debug", "release"]:
    238       print "Unknown mode %s" % mode
    239       return False
    240   if options.arch in ["auto", "native"]:
    241     options.arch = ARCH_GUESS
    242   options.arch = options.arch.split(",")
    243   for arch in options.arch:
    244     if not arch in SUPPORTED_ARCHS:
    245       print "Unknown architecture %s" % arch
    246       return False
    247 
    248   # Special processing of other options, sorted alphabetically.
    249   options.command_prefix = shlex.split(options.command_prefix)
    250   options.extra_flags = shlex.split(options.extra_flags)
    251   if options.j == 0:
    252     options.j = multiprocessing.cpu_count()
    253   while options.random_seed == 0:
    254     options.random_seed = random.SystemRandom().randint(-2147483648, 2147483647)
    255   if not options.distribution_mode in DISTRIBUTION_MODES:
    256     print "Unknown distribution mode %s" % options.distribution_mode
    257     return False
    258   if options.distribution_factor1 < 0.0:
    259     print ("Distribution factor1 %s is out of range. Defaulting to 0.0"
    260         % options.distribution_factor1)
    261     options.distribution_factor1 = 0.0
    262   if options.distribution_factor2 < 0.0:
    263     print ("Distribution factor2 %s is out of range. Defaulting to 0.0"
    264         % options.distribution_factor2)
    265     options.distribution_factor2 = 0.0
    266   if options.coverage < 0.0 or options.coverage > 1.0:
    267     print ("Coverage %s is out of range. Defaulting to 0.4"
    268         % options.coverage)
    269     options.coverage = 0.4
    270   if options.coverage_lift < 0:
    271     print ("Coverage lift %s is out of range. Defaulting to 0"
    272         % options.coverage_lift)
    273     options.coverage_lift = 0
    274   return True
    275 
    276 
    277 def ShardTests(tests, shard_count, shard_run):
    278   if shard_count < 2:
    279     return tests
    280   if shard_run < 1 or shard_run > shard_count:
    281     print "shard-run not a valid number, should be in [1:shard-count]"
    282     print "defaulting back to running all tests"
    283     return tests
    284   count = 0
    285   shard = []
    286   for test in tests:
    287     if count % shard_count == shard_run - 1:
    288       shard.append(test)
    289     count += 1
    290   return shard
    291 
    292 
    293 def Main():
    294   # Use the v8 root as cwd as some test cases use "load" with relative paths.
    295   os.chdir(BASE_DIR)
    296 
    297   parser = BuildOptions()
    298   (options, args) = parser.parse_args()
    299   if not ProcessOptions(options):
    300     parser.print_help()
    301     return 1
    302 
    303   exit_code = 0
    304 
    305   suite_paths = utils.GetSuitePaths(join(BASE_DIR, "test"))
    306 
    307   if len(args) == 0:
    308     suite_paths = [ s for s in suite_paths if s in DEFAULT_TESTS ]
    309   else:
    310     args_suites = set()
    311     for arg in args:
    312       suite = arg.split(os.path.sep)[0]
    313       if not suite in args_suites:
    314         args_suites.add(suite)
    315     suite_paths = [ s for s in suite_paths if s in args_suites ]
    316 
    317   suites = []
    318   for root in suite_paths:
    319     suite = testsuite.TestSuite.LoadTestSuite(
    320         os.path.join(BASE_DIR, "test", root))
    321     if suite:
    322       suites.append(suite)
    323 
    324   if options.download_data:
    325     for s in suites:
    326       s.DownloadData()
    327 
    328   for mode in options.mode:
    329     for arch in options.arch:
    330       try:
    331         code = Execute(arch, mode, args, options, suites, BASE_DIR)
    332         exit_code = exit_code or code
    333       except KeyboardInterrupt:
    334         return 2
    335   return exit_code
    336 
    337 
    338 def CalculateNTests(m, options):
    339   """Calculates the number of tests from m deopt points with exponential
    340   coverage.
    341   The coverage is expected to be between 0.0 and 1.0.
    342   The 'coverage lift' lifts the coverage for tests with smaller m values.
    343   """
    344   c = float(options.coverage)
    345   l = float(options.coverage_lift)
    346   return int(math.pow(m, (m * c + l) / (m + l)))
    347 
    348 
    349 def Execute(arch, mode, args, options, suites, workspace):
    350   print(">>> Running tests for %s.%s" % (arch, mode))
    351 
    352   dist = Distribution(options)
    353 
    354   shell_dir = options.shell_dir
    355   if not shell_dir:
    356     if options.buildbot:
    357       shell_dir = os.path.join(workspace, options.outdir, mode)
    358       mode = mode.lower()
    359     else:
    360       shell_dir = os.path.join(workspace, options.outdir,
    361                                "%s.%s" % (arch, mode))
    362   shell_dir = os.path.relpath(shell_dir)
    363 
    364   # Populate context object.
    365   mode_flags = MODE_FLAGS[mode]
    366   timeout = options.timeout
    367   if timeout == -1:
    368     # Simulators are slow, therefore allow a longer default timeout.
    369     if arch in SLOW_ARCHS:
    370       timeout = 2 * TIMEOUT_DEFAULT;
    371     else:
    372       timeout = TIMEOUT_DEFAULT;
    373 
    374   timeout *= TIMEOUT_SCALEFACTOR[mode]
    375   ctx = context.Context(arch, mode, shell_dir,
    376                         mode_flags, options.verbose,
    377                         timeout, options.isolates,
    378                         options.command_prefix,
    379                         options.extra_flags,
    380                         False,  # Keep i18n on by default.
    381                         options.random_seed,
    382                         True,  # No sorting of test cases.
    383                         0,  # Don't rerun failing tests.
    384                         0,  # No use of a rerun-failing-tests maximum.
    385                         False,  # No predictable mode.
    386                         False,  # No no_harness mode.
    387                         False,  # Don't use perf data.
    388                         False)  # Coverage not supported.
    389 
    390   # Find available test suites and read test cases from them.
    391   variables = {
    392     "arch": arch,
    393     "asan": options.asan,
    394     "deopt_fuzzer": True,
    395     "gc_stress": False,
    396     "gcov_coverage": False,
    397     "isolates": options.isolates,
    398     "mode": mode,
    399     "no_i18n": False,
    400     "no_snap": False,
    401     "simulator": utils.UseSimulator(arch),
    402     "system": utils.GuessOS(),
    403     "tsan": False,
    404     "msan": False,
    405     "dcheck_always_on": options.dcheck_always_on,
    406     "novfp3": False,
    407     "predictable": False,
    408     "byteorder": sys.byteorder,
    409   }
    410   all_tests = []
    411   num_tests = 0
    412   test_id = 0
    413 
    414   # Remember test case prototypes for the fuzzing phase.
    415   test_backup = dict((s, []) for s in suites)
    416 
    417   for s in suites:
    418     s.ReadStatusFile(variables)
    419     s.ReadTestCases(ctx)
    420     if len(args) > 0:
    421       s.FilterTestCasesByArgs(args)
    422     all_tests += s.tests
    423     s.FilterTestCasesByStatus(False)
    424     test_backup[s] = s.tests
    425     analysis_flags = ["--deopt-every-n-times", "%d" % MAX_DEOPT,
    426                       "--print-deopt-stress"]
    427     s.tests = [ t.CopyAddingFlags(t.variant, analysis_flags) for t in s.tests ]
    428     num_tests += len(s.tests)
    429     for t in s.tests:
    430       t.id = test_id
    431       test_id += 1
    432 
    433   if num_tests == 0:
    434     print "No tests to run."
    435     return 0
    436 
    437   print(">>> Collection phase")
    438   progress_indicator = progress.PROGRESS_INDICATORS[options.progress]()
    439   runner = execution.Runner(suites, progress_indicator, ctx)
    440 
    441   exit_code = runner.Run(options.j)
    442 
    443   print(">>> Analysis phase")
    444   num_tests = 0
    445   test_id = 0
    446   for s in suites:
    447     test_results = {}
    448     for t in s.tests:
    449       for line in t.output.stdout.splitlines():
    450         if line.startswith("=== Stress deopt counter: "):
    451           test_results[t.path] = MAX_DEOPT - int(line.split(" ")[-1])
    452     for t in s.tests:
    453       if t.path not in test_results:
    454         print "Missing results for %s" % t.path
    455     if options.dump_results_file:
    456       results_dict = dict((t.path, n) for (t, n) in test_results.iteritems())
    457       with file("%s.%d.txt" % (dump_results_file, time.time()), "w") as f:
    458         f.write(json.dumps(results_dict))
    459 
    460     # Reset tests and redistribute the prototypes from the collection phase.
    461     s.tests = []
    462     if options.verbose:
    463       print "Test distributions:"
    464     for t in test_backup[s]:
    465       max_deopt = test_results.get(t.path, 0)
    466       if max_deopt == 0:
    467         continue
    468       n_deopt = CalculateNTests(max_deopt, options)
    469       distribution = dist.Distribute(n_deopt, max_deopt)
    470       if options.verbose:
    471         print "%s %s" % (t.path, distribution)
    472       for i in distribution:
    473         fuzzing_flags = ["--deopt-every-n-times", "%d" % i]
    474         s.tests.append(t.CopyAddingFlags(t.variant, fuzzing_flags))
    475     num_tests += len(s.tests)
    476     for t in s.tests:
    477       t.id = test_id
    478       test_id += 1
    479 
    480   if num_tests == 0:
    481     print "No tests to run."
    482     return 0
    483 
    484   print(">>> Deopt fuzzing phase (%d test cases)" % num_tests)
    485   progress_indicator = progress.PROGRESS_INDICATORS[options.progress]()
    486   runner = execution.Runner(suites, progress_indicator, ctx)
    487 
    488   code = runner.Run(options.j)
    489   return exit_code or code
    490 
    491 
    492 if __name__ == "__main__":
    493   sys.exit(Main())
    494