| #!/usr/bin/env python | 
 | # | 
 | # Copyright 2012 the V8 project authors. All rights reserved. | 
 | # Redistribution and use in source and binary forms, with or without | 
 | # modification, are permitted provided that the following conditions are | 
 | # met: | 
 | # | 
 | #     * Redistributions of source code must retain the above copyright | 
 | #       notice, this list of conditions and the following disclaimer. | 
 | #     * Redistributions in binary form must reproduce the above | 
 | #       copyright notice, this list of conditions and the following | 
 | #       disclaimer in the documentation and/or other materials provided | 
 | #       with the distribution. | 
 | #     * Neither the name of Google Inc. nor the names of its | 
 | #       contributors may be used to endorse or promote products derived | 
 | #       from this software without specific prior written permission. | 
 | # | 
 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 
 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 
 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | 
 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | 
 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 
 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
 |  | 
 |  | 
 | import json | 
 | import math | 
 | import multiprocessing | 
 | import optparse | 
 | import os | 
 | from os.path import join | 
 | import random | 
 | import shlex | 
 | import subprocess | 
 | import sys | 
 | import time | 
 |  | 
 | from testrunner.local import execution | 
 | from testrunner.local import progress | 
 | from testrunner.local import testsuite | 
 | from testrunner.local import utils | 
 | from testrunner.local import verbose | 
 | from testrunner.objects import context | 
 |  | 
 |  | 
 | # Base dir of the v8 checkout to be used as cwd. | 
 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 
 |  | 
 | ARCH_GUESS = utils.DefaultArch() | 
 | DEFAULT_TESTS = ["mjsunit", "webkit"] | 
 | TIMEOUT_DEFAULT = 60 | 
 | TIMEOUT_SCALEFACTOR = {"debug"   : 4, | 
 |                        "release" : 1 } | 
 |  | 
 | MODE_FLAGS = { | 
 |     "debug"   : ["--nohard-abort", "--enable-slow-asserts", | 
 |                  "--verify-heap", "--noconcurrent-recompilation"], | 
 |     "release" : ["--nohard-abort", "--noconcurrent-recompilation"]} | 
 |  | 
 | SUPPORTED_ARCHS = ["android_arm", | 
 |                    "android_ia32", | 
 |                    "arm", | 
 |                    "ia32", | 
 |                    "ppc", | 
 |                    "ppc64", | 
 |                    "s390", | 
 |                    "s390x", | 
 |                    "mipsel", | 
 |                    "x64"] | 
 | # Double the timeout for these: | 
 | SLOW_ARCHS = ["android_arm", | 
 |               "android_ia32", | 
 |               "arm", | 
 |               "mipsel"] | 
 | MAX_DEOPT = 1000000000 | 
 | DISTRIBUTION_MODES = ["smooth", "random"] | 
 |  | 
 |  | 
 | class RandomDistribution: | 
 |   def __init__(self, seed=None): | 
 |     seed = seed or random.randint(1, sys.maxint) | 
 |     print "Using random distribution with seed %d" % seed | 
 |     self._random = random.Random(seed) | 
 |  | 
 |   def Distribute(self, n, m): | 
 |     if n > m: | 
 |       n = m | 
 |     return self._random.sample(xrange(1, m + 1), n) | 
 |  | 
 |  | 
 | class SmoothDistribution: | 
 |   """Distribute n numbers into the interval [1:m]. | 
 |   F1: Factor of the first derivation of the distribution function. | 
 |   F2: Factor of the second derivation of the distribution function. | 
 |   With F1 and F2 set to 0, the distribution will be equal. | 
 |   """ | 
 |   def __init__(self, factor1=2.0, factor2=0.2): | 
 |     self._factor1 = factor1 | 
 |     self._factor2 = factor2 | 
 |  | 
 |   def Distribute(self, n, m): | 
 |     if n > m: | 
 |       n = m | 
 |     if n <= 1: | 
 |       return [ 1 ] | 
 |  | 
 |     result = [] | 
 |     x = 0.0 | 
 |     dx = 1.0 | 
 |     ddx = self._factor1 | 
 |     dddx = self._factor2 | 
 |     for i in range(0, n): | 
 |       result += [ x ] | 
 |       x += dx | 
 |       dx += ddx | 
 |       ddx += dddx | 
 |  | 
 |     # Project the distribution into the interval [0:M]. | 
 |     result = [ x * m / result[-1] for x in result ] | 
 |  | 
 |     # Equalize by n. The closer n is to m, the more equal will be the | 
 |     # distribution. | 
 |     for (i, x) in enumerate(result): | 
 |       # The value of x if it was equally distributed. | 
 |       equal_x = i / float(n - 1) * float(m - 1) + 1 | 
 |  | 
 |       # Difference factor between actual and equal distribution. | 
 |       diff = 1 - (x / equal_x) | 
 |  | 
 |       # Equalize x dependent on the number of values to distribute. | 
 |       result[i] = int(x + (i + 1) * diff) | 
 |     return result | 
 |  | 
 |  | 
 | def Distribution(options): | 
 |   if options.distribution_mode == "random": | 
 |     return RandomDistribution(options.seed) | 
 |   if options.distribution_mode == "smooth": | 
 |     return SmoothDistribution(options.distribution_factor1, | 
 |                               options.distribution_factor2) | 
 |  | 
 |  | 
 | def BuildOptions(): | 
 |   result = optparse.OptionParser() | 
 |   result.add_option("--arch", | 
 |                     help=("The architecture to run tests for, " | 
 |                           "'auto' or 'native' for auto-detect"), | 
 |                     default="ia32,x64,arm") | 
 |   result.add_option("--arch-and-mode", | 
 |                     help="Architecture and mode in the format 'arch.mode'", | 
 |                     default=None) | 
 |   result.add_option("--asan", | 
 |                     help="Regard test expectations for ASAN", | 
 |                     default=False, action="store_true") | 
 |   result.add_option("--buildbot", | 
 |                     help="Adapt to path structure used on buildbots", | 
 |                     default=False, action="store_true") | 
 |   result.add_option("--dcheck-always-on", | 
 |                     help="Indicates that V8 was compiled with DCHECKs enabled", | 
 |                     default=False, action="store_true") | 
 |   result.add_option("--command-prefix", | 
 |                     help="Prepended to each shell command used to run a test", | 
 |                     default="") | 
 |   result.add_option("--coverage", help=("Exponential test coverage " | 
 |                     "(range 0.0, 1.0) -- 0.0: one test, 1.0 all tests (slow)"), | 
 |                     default=0.4, type="float") | 
 |   result.add_option("--coverage-lift", help=("Lifts test coverage for tests " | 
 |                     "with a small number of deopt points (range 0, inf)"), | 
 |                     default=20, type="int") | 
 |   result.add_option("--download-data", help="Download missing test suite data", | 
 |                     default=False, action="store_true") | 
 |   result.add_option("--distribution-factor1", help=("Factor of the first " | 
 |                     "derivation of the distribution function"), default=2.0, | 
 |                     type="float") | 
 |   result.add_option("--distribution-factor2", help=("Factor of the second " | 
 |                     "derivation of the distribution function"), default=0.7, | 
 |                     type="float") | 
 |   result.add_option("--distribution-mode", help=("How to select deopt points " | 
 |                     "for a given test (smooth|random)"), | 
 |                     default="smooth") | 
 |   result.add_option("--dump-results-file", help=("Dump maximum number of " | 
 |                     "deopt points per test to a file")) | 
 |   result.add_option("--extra-flags", | 
 |                     help="Additional flags to pass to each test command", | 
 |                     default="") | 
 |   result.add_option("--isolates", help="Whether to test isolates", | 
 |                     default=False, action="store_true") | 
 |   result.add_option("-j", help="The number of parallel tasks to run", | 
 |                     default=0, type="int") | 
 |   result.add_option("-m", "--mode", | 
 |                     help="The test modes in which to run (comma-separated)", | 
 |                     default="release,debug") | 
 |   result.add_option("--outdir", help="Base directory with compile output", | 
 |                     default="out") | 
 |   result.add_option("-p", "--progress", | 
 |                     help=("The style of progress indicator" | 
 |                           " (verbose, dots, color, mono)"), | 
 |                     choices=progress.PROGRESS_INDICATORS.keys(), | 
 |                     default="mono") | 
 |   result.add_option("--shard-count", | 
 |                     help="Split testsuites into this number of shards", | 
 |                     default=1, type="int") | 
 |   result.add_option("--shard-run", | 
 |                     help="Run this shard from the split up tests.", | 
 |                     default=1, type="int") | 
 |   result.add_option("--shell-dir", help="Directory containing executables", | 
 |                     default="") | 
 |   result.add_option("--seed", help="The seed for the random distribution", | 
 |                     type="int") | 
 |   result.add_option("-t", "--timeout", help="Timeout in seconds", | 
 |                     default= -1, type="int") | 
 |   result.add_option("-v", "--verbose", help="Verbose output", | 
 |                     default=False, action="store_true") | 
 |   result.add_option("--random-seed", default=0, dest="random_seed", | 
 |                     help="Default seed for initializing random generator") | 
 |   return result | 
 |  | 
 |  | 
 | def ProcessOptions(options): | 
 |   global VARIANT_FLAGS | 
 |  | 
 |   # Architecture and mode related stuff. | 
 |   if options.arch_and_mode: | 
 |     tokens = options.arch_and_mode.split(".") | 
 |     options.arch = tokens[0] | 
 |     options.mode = tokens[1] | 
 |   options.mode = options.mode.split(",") | 
 |   for mode in options.mode: | 
 |     if not mode.lower() in ["debug", "release"]: | 
 |       print "Unknown mode %s" % mode | 
 |       return False | 
 |   if options.arch in ["auto", "native"]: | 
 |     options.arch = ARCH_GUESS | 
 |   options.arch = options.arch.split(",") | 
 |   for arch in options.arch: | 
 |     if not arch in SUPPORTED_ARCHS: | 
 |       print "Unknown architecture %s" % arch | 
 |       return False | 
 |  | 
 |   # Special processing of other options, sorted alphabetically. | 
 |   options.command_prefix = shlex.split(options.command_prefix) | 
 |   options.extra_flags = shlex.split(options.extra_flags) | 
 |   if options.j == 0: | 
 |     options.j = multiprocessing.cpu_count() | 
 |   while options.random_seed == 0: | 
 |     options.random_seed = random.SystemRandom().randint(-2147483648, 2147483647) | 
 |   if not options.distribution_mode in DISTRIBUTION_MODES: | 
 |     print "Unknown distribution mode %s" % options.distribution_mode | 
 |     return False | 
 |   if options.distribution_factor1 < 0.0: | 
 |     print ("Distribution factor1 %s is out of range. Defaulting to 0.0" | 
 |         % options.distribution_factor1) | 
 |     options.distribution_factor1 = 0.0 | 
 |   if options.distribution_factor2 < 0.0: | 
 |     print ("Distribution factor2 %s is out of range. Defaulting to 0.0" | 
 |         % options.distribution_factor2) | 
 |     options.distribution_factor2 = 0.0 | 
 |   if options.coverage < 0.0 or options.coverage > 1.0: | 
 |     print ("Coverage %s is out of range. Defaulting to 0.4" | 
 |         % options.coverage) | 
 |     options.coverage = 0.4 | 
 |   if options.coverage_lift < 0: | 
 |     print ("Coverage lift %s is out of range. Defaulting to 0" | 
 |         % options.coverage_lift) | 
 |     options.coverage_lift = 0 | 
 |   return True | 
 |  | 
 |  | 
 | def ShardTests(tests, shard_count, shard_run): | 
 |   if shard_count < 2: | 
 |     return tests | 
 |   if shard_run < 1 or shard_run > shard_count: | 
 |     print "shard-run not a valid number, should be in [1:shard-count]" | 
 |     print "defaulting back to running all tests" | 
 |     return tests | 
 |   count = 0 | 
 |   shard = [] | 
 |   for test in tests: | 
 |     if count % shard_count == shard_run - 1: | 
 |       shard.append(test) | 
 |     count += 1 | 
 |   return shard | 
 |  | 
 |  | 
 | def Main(): | 
 |   # Use the v8 root as cwd as some test cases use "load" with relative paths. | 
 |   os.chdir(BASE_DIR) | 
 |  | 
 |   parser = BuildOptions() | 
 |   (options, args) = parser.parse_args() | 
 |   if not ProcessOptions(options): | 
 |     parser.print_help() | 
 |     return 1 | 
 |  | 
 |   exit_code = 0 | 
 |  | 
 |   suite_paths = utils.GetSuitePaths(join(BASE_DIR, "test")) | 
 |  | 
 |   if len(args) == 0: | 
 |     suite_paths = [ s for s in suite_paths if s in DEFAULT_TESTS ] | 
 |   else: | 
 |     args_suites = set() | 
 |     for arg in args: | 
 |       suite = arg.split(os.path.sep)[0] | 
 |       if not suite in args_suites: | 
 |         args_suites.add(suite) | 
 |     suite_paths = [ s for s in suite_paths if s in args_suites ] | 
 |  | 
 |   suites = [] | 
 |   for root in suite_paths: | 
 |     suite = testsuite.TestSuite.LoadTestSuite( | 
 |         os.path.join(BASE_DIR, "test", root)) | 
 |     if suite: | 
 |       suites.append(suite) | 
 |  | 
 |   if options.download_data: | 
 |     for s in suites: | 
 |       s.DownloadData() | 
 |  | 
 |   for mode in options.mode: | 
 |     for arch in options.arch: | 
 |       try: | 
 |         code = Execute(arch, mode, args, options, suites, BASE_DIR) | 
 |         exit_code = exit_code or code | 
 |       except KeyboardInterrupt: | 
 |         return 2 | 
 |   return exit_code | 
 |  | 
 |  | 
 | def CalculateNTests(m, options): | 
 |   """Calculates the number of tests from m deopt points with exponential | 
 |   coverage. | 
 |   The coverage is expected to be between 0.0 and 1.0. | 
 |   The 'coverage lift' lifts the coverage for tests with smaller m values. | 
 |   """ | 
 |   c = float(options.coverage) | 
 |   l = float(options.coverage_lift) | 
 |   return int(math.pow(m, (m * c + l) / (m + l))) | 
 |  | 
 |  | 
 | def Execute(arch, mode, args, options, suites, workspace): | 
 |   print(">>> Running tests for %s.%s" % (arch, mode)) | 
 |  | 
 |   dist = Distribution(options) | 
 |  | 
 |   shell_dir = options.shell_dir | 
 |   if not shell_dir: | 
 |     if options.buildbot: | 
 |       shell_dir = os.path.join(workspace, options.outdir, mode) | 
 |       mode = mode.lower() | 
 |     else: | 
 |       shell_dir = os.path.join(workspace, options.outdir, | 
 |                                "%s.%s" % (arch, mode)) | 
 |   shell_dir = os.path.relpath(shell_dir) | 
 |  | 
 |   # Populate context object. | 
 |   mode_flags = MODE_FLAGS[mode] | 
 |   timeout = options.timeout | 
 |   if timeout == -1: | 
 |     # Simulators are slow, therefore allow a longer default timeout. | 
 |     if arch in SLOW_ARCHS: | 
 |       timeout = 2 * TIMEOUT_DEFAULT; | 
 |     else: | 
 |       timeout = TIMEOUT_DEFAULT; | 
 |  | 
 |   timeout *= TIMEOUT_SCALEFACTOR[mode] | 
 |   ctx = context.Context(arch, mode, shell_dir, | 
 |                         mode_flags, options.verbose, | 
 |                         timeout, options.isolates, | 
 |                         options.command_prefix, | 
 |                         options.extra_flags, | 
 |                         False,  # Keep i18n on by default. | 
 |                         options.random_seed, | 
 |                         True,  # No sorting of test cases. | 
 |                         0,  # Don't rerun failing tests. | 
 |                         0,  # No use of a rerun-failing-tests maximum. | 
 |                         False,  # No predictable mode. | 
 |                         False,  # No no_harness mode. | 
 |                         False,  # Don't use perf data. | 
 |                         False)  # Coverage not supported. | 
 |  | 
 |   # Find available test suites and read test cases from them. | 
 |   variables = { | 
 |     "arch": arch, | 
 |     "asan": options.asan, | 
 |     "deopt_fuzzer": True, | 
 |     "gc_stress": False, | 
 |     "gcov_coverage": False, | 
 |     "isolates": options.isolates, | 
 |     "mode": mode, | 
 |     "no_i18n": False, | 
 |     "no_snap": False, | 
 |     "simulator": utils.UseSimulator(arch), | 
 |     "system": utils.GuessOS(), | 
 |     "tsan": False, | 
 |     "msan": False, | 
 |     "dcheck_always_on": options.dcheck_always_on, | 
 |     "novfp3": False, | 
 |     "predictable": False, | 
 |     "byteorder": sys.byteorder, | 
 |     "no_harness": False, | 
 |     "ubsan_vptr": False, | 
 |   } | 
 |   all_tests = [] | 
 |   num_tests = 0 | 
 |   test_id = 0 | 
 |  | 
 |   # Remember test case prototypes for the fuzzing phase. | 
 |   test_backup = dict((s, []) for s in suites) | 
 |  | 
 |   for s in suites: | 
 |     s.ReadStatusFile(variables) | 
 |     s.ReadTestCases(ctx) | 
 |     if len(args) > 0: | 
 |       s.FilterTestCasesByArgs(args) | 
 |     all_tests += s.tests | 
 |     s.FilterTestCasesByStatus(False) | 
 |     test_backup[s] = s.tests | 
 |     analysis_flags = ["--deopt-every-n-times", "%d" % MAX_DEOPT, | 
 |                       "--print-deopt-stress"] | 
 |     s.tests = [ t.CopyAddingFlags(t.variant, analysis_flags) for t in s.tests ] | 
 |     num_tests += len(s.tests) | 
 |     for t in s.tests: | 
 |       t.id = test_id | 
 |       test_id += 1 | 
 |  | 
 |   if num_tests == 0: | 
 |     print "No tests to run." | 
 |     return 0 | 
 |  | 
 |   print(">>> Collection phase") | 
 |   progress_indicator = progress.PROGRESS_INDICATORS[options.progress]() | 
 |   runner = execution.Runner(suites, progress_indicator, ctx) | 
 |  | 
 |   exit_code = runner.Run(options.j) | 
 |  | 
 |   print(">>> Analysis phase") | 
 |   num_tests = 0 | 
 |   test_id = 0 | 
 |   for s in suites: | 
 |     test_results = {} | 
 |     for t in s.tests: | 
 |       for line in t.output.stdout.splitlines(): | 
 |         if line.startswith("=== Stress deopt counter: "): | 
 |           test_results[t.path] = MAX_DEOPT - int(line.split(" ")[-1]) | 
 |     for t in s.tests: | 
 |       if t.path not in test_results: | 
 |         print "Missing results for %s" % t.path | 
 |     if options.dump_results_file: | 
 |       results_dict = dict((t.path, n) for (t, n) in test_results.iteritems()) | 
 |       with file("%s.%d.txt" % (dump_results_file, time.time()), "w") as f: | 
 |         f.write(json.dumps(results_dict)) | 
 |  | 
 |     # Reset tests and redistribute the prototypes from the collection phase. | 
 |     s.tests = [] | 
 |     if options.verbose: | 
 |       print "Test distributions:" | 
 |     for t in test_backup[s]: | 
 |       max_deopt = test_results.get(t.path, 0) | 
 |       if max_deopt == 0: | 
 |         continue | 
 |       n_deopt = CalculateNTests(max_deopt, options) | 
 |       distribution = dist.Distribute(n_deopt, max_deopt) | 
 |       if options.verbose: | 
 |         print "%s %s" % (t.path, distribution) | 
 |       for i in distribution: | 
 |         fuzzing_flags = ["--deopt-every-n-times", "%d" % i] | 
 |         s.tests.append(t.CopyAddingFlags(t.variant, fuzzing_flags)) | 
 |     num_tests += len(s.tests) | 
 |     for t in s.tests: | 
 |       t.id = test_id | 
 |       test_id += 1 | 
 |  | 
 |   if num_tests == 0: | 
 |     print "No tests to run." | 
 |     return 0 | 
 |  | 
 |   print(">>> Deopt fuzzing phase (%d test cases)" % num_tests) | 
 |   progress_indicator = progress.PROGRESS_INDICATORS[options.progress]() | 
 |   runner = execution.Runner(suites, progress_indicator, ctx) | 
 |  | 
 |   code = runner.Run(options.j) | 
 |   return exit_code or code | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |   sys.exit(Main()) |