| #!/usr/bin/env python | 
 | # | 
 | # Copyright 2017 the V8 project authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 |  | 
 | from os.path import join | 
 | import json | 
 | import math | 
 | import multiprocessing | 
 | import os | 
 | import random | 
 | import shlex | 
 | import sys | 
 | import time | 
 |  | 
 | # Adds testrunner to the path hence it has to be imported at the beggining. | 
 | import base_runner | 
 |  | 
 | 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 | 
 |  | 
 |  | 
 | DEFAULT_SUITES = ["mjsunit", "webkit"] | 
 | TIMEOUT_DEFAULT = 60 | 
 |  | 
 | # Double the timeout for these: | 
 | SLOW_ARCHS = ["arm", | 
 |               "mipsel"] | 
 | MAX_DEOPT = 1000000000 | 
 | DISTRIBUTION_MODES = ["smooth", "random"] | 
 |  | 
 |  | 
 | class DeoptFuzzer(base_runner.BaseTestRunner): | 
 |   def __init__(self, *args, **kwargs): | 
 |     super(DeoptFuzzer, self).__init__(*args, **kwargs) | 
 |  | 
 |   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(self, options): | 
 |     if options.distribution_mode == "random": | 
 |       return self.RandomDistribution(options.seed) | 
 |     if options.distribution_mode == "smooth": | 
 |       return self.SmoothDistribution(options.distribution_factor1, | 
 |                                      options.distribution_factor2) | 
 |  | 
 |  | 
 |   def _add_parser_options(self, parser): | 
 |     parser.add_option("--command-prefix", | 
 |                       help="Prepended to each shell command used to run a test", | 
 |                       default="") | 
 |     parser.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") | 
 |     parser.add_option("--coverage-lift", help=("Lifts test coverage for tests " | 
 |                       "with a small number of deopt points (range 0, inf)"), | 
 |                       default=20, type="int") | 
 |     parser.add_option("--distribution-factor1", help=("Factor of the first " | 
 |                       "derivation of the distribution function"), default=2.0, | 
 |                       type="float") | 
 |     parser.add_option("--distribution-factor2", help=("Factor of the second " | 
 |                       "derivation of the distribution function"), default=0.7, | 
 |                       type="float") | 
 |     parser.add_option("--distribution-mode", help=("How to select deopt points " | 
 |                       "for a given test (smooth|random)"), | 
 |                       default="smooth") | 
 |     parser.add_option("--dump-results-file", help=("Dump maximum number of " | 
 |                       "deopt points per test to a file")) | 
 |     parser.add_option("--extra-flags", | 
 |                       help="Additional flags to pass to each test command", | 
 |                       default="") | 
 |     parser.add_option("--isolates", help="Whether to test isolates", | 
 |                       default=False, action="store_true") | 
 |     parser.add_option("-j", help="The number of parallel tasks to run", | 
 |                       default=0, type="int") | 
 |     parser.add_option("-p", "--progress", | 
 |                       help=("The style of progress indicator" | 
 |                             " (verbose, dots, color, mono)"), | 
 |                       choices=progress.PROGRESS_INDICATORS.keys(), | 
 |                       default="mono") | 
 |     parser.add_option("--seed", help="The seed for the random distribution", | 
 |                       type="int") | 
 |     parser.add_option("-t", "--timeout", help="Timeout in seconds", | 
 |                       default= -1, type="int") | 
 |     parser.add_option("--random-seed", default=0, dest="random_seed", | 
 |                       help="Default seed for initializing random generator") | 
 |     parser.add_option("--fuzzer-random-seed", default=0, | 
 |                       help="Default seed for initializing fuzzer random " | 
 |                       "generator") | 
 |     return parser | 
 |  | 
 |  | 
 |   def _process_options(self, options): | 
 |     # 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 _calculate_n_tests(self, 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 _get_default_suite_names(self): | 
 |     return DEFAULT_SUITES | 
 |  | 
 |   def _do_execute(self, suites, args, options): | 
 |     print(">>> Running tests for %s.%s" % (self.build_config.arch, | 
 |                                            self.mode_name)) | 
 |  | 
 |     dist = self._distribution(options) | 
 |  | 
 |     # Populate context object. | 
 |     timeout = options.timeout | 
 |     if timeout == -1: | 
 |       # Simulators are slow, therefore allow a longer default timeout. | 
 |       if self.build_config.arch in SLOW_ARCHS: | 
 |         timeout = 2 * TIMEOUT_DEFAULT; | 
 |       else: | 
 |         timeout = TIMEOUT_DEFAULT; | 
 |  | 
 |     timeout *= self.mode_options.timeout_scalefactor | 
 |     ctx = context.Context(self.build_config.arch, | 
 |                           self.mode_options.execution_mode, | 
 |                           self.outdir, | 
 |                           self.mode_options.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 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": self.build_config.arch, | 
 |       "asan": self.build_config.asan, | 
 |       "byteorder": sys.byteorder, | 
 |       "dcheck_always_on": self.build_config.dcheck_always_on, | 
 |       "deopt_fuzzer": True, | 
 |       "gc_fuzzer": False, | 
 |       "gc_stress": False, | 
 |       "gcov_coverage": self.build_config.gcov_coverage, | 
 |       "isolates": options.isolates, | 
 |       "mode": self.mode_options.status_mode, | 
 |       "msan": self.build_config.msan, | 
 |       "no_harness": False, | 
 |       "no_i18n": self.build_config.no_i18n, | 
 |       "no_snap": self.build_config.no_snap, | 
 |       "novfp3": False, | 
 |       "predictable": self.build_config.predictable, | 
 |       "simulator": utils.UseSimulator(self.build_config.arch), | 
 |       "simulator_run": False, | 
 |       "system": utils.GuessOS(), | 
 |       "tsan": self.build_config.tsan, | 
 |       "ubsan_vptr": self.build_config.ubsan_vptr, | 
 |     } | 
 |     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) | 
 |       s.FilterTestCasesByStatus(False) | 
 |  | 
 |       test_backup[s] = s.tests | 
 |       analysis_flags = ["--deopt-every-n-times", "%d" % MAX_DEOPT, | 
 |                         "--print-deopt-stress"] | 
 |       s.tests = [t.create_variant(t.variant, analysis_flags, 'analysis') | 
 |                  for t in s.tests] | 
 |       num_tests += len(s.tests) | 
 |       for t in s.tests: | 
 |         t.id = test_id | 
 |         t.cmd = t.get_command(ctx) | 
 |         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 runner.outputs[t].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" % (options.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 = self._calculate_n_tests(max_deopt, options) | 
 |         distribution = dist.Distribute(n_deopt, max_deopt) | 
 |         if options.verbose: | 
 |           print "%s %s" % (t.path, distribution) | 
 |         for n, d in enumerate(distribution): | 
 |           fuzzing_flags = ["--deopt-every-n-times", "%d" % d] | 
 |           s.tests.append(t.create_variant(t.variant, fuzzing_flags, n)) | 
 |       num_tests += len(s.tests) | 
 |       for t in s.tests: | 
 |         t.id = test_id | 
 |         t.cmd = t.get_command(ctx) | 
 |         test_id += 1 | 
 |  | 
 |     if num_tests == 0: | 
 |       print "No tests to run." | 
 |       return exit_code | 
 |  | 
 |     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(DeoptFuzzer().execute()) |