#!/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 itertools 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", "benchmarks"] TIMEOUT_DEFAULT = 60 # Double the timeout for these: SLOW_ARCHS = ["arm", "mipsel"] class GCFuzzer(base_runner.BaseTestRunner): def __init__(self, *args, **kwargs): super(GCFuzzer, self).__init__(*args, **kwargs) self.fuzzer_rng = None 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 low memory size reached (range 0, inf)"), default=20, type="int") parser.add_option("--dump-results-file", help="Dump maximum limit reached") 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("-t", "--timeout", help="Timeout in seconds", default= -1, type="int") parser.add_option("--random-seed", default=0, help="Default seed for initializing random generator") parser.add_option("--fuzzer-random-seed", default=0, help="Default seed for initializing fuzzer random " "generator") parser.add_option("--stress-compaction", default=False, action="store_true", help="Enable stress_compaction_percentage flag") parser.add_option("--distribution-factor1", help="DEPRECATED") parser.add_option("--distribution-factor2", help="DEPRECATED") parser.add_option("--distribution-mode", help="DEPRECATED") parser.add_option("--seed", help="DEPRECATED") 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) while options.fuzzer_random_seed == 0: options.fuzzer_random_seed = random.SystemRandom().randint(-2147483648, 2147483647) self.fuzzer_rng = random.Random(options.fuzzer_random_seed) return True def _calculate_n_tests(self, m, options): """Calculates the number of tests from m 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)) # 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. num_tests = self._load_tests(args, options, suites, ctx) if num_tests == 0: print "No tests to run." return 0 test_backup = dict(map(lambda s: (s, s.tests), suites)) print('>>> Collection phase') for s in suites: analysis_flags = ['--fuzzer-gc-analysis'] s.tests = map(lambda t: t.create_variant(t.variant, analysis_flags, 'analysis'), s.tests) for t in s.tests: t.cmd = t.get_command(ctx) progress_indicator = progress.PROGRESS_INDICATORS[options.progress]() runner = execution.Runner(suites, progress_indicator, ctx) exit_code = runner.Run(options.j) print('>>> Analysis phase') test_results = dict() for s in suites: for t in s.tests: # Skip failed tests. if t.output_proc.has_unexpected_output(runner.outputs[t]): print '%s failed, skipping' % t.path continue max_limit = self._get_max_limit_reached(runner.outputs[t]) if max_limit: test_results[t.path] = max_limit runner = None if options.dump_results_file: with file("%s.%d.txt" % (options.dump_results_file, time.time()), "w") as f: f.write(json.dumps(test_results)) num_tests = 0 for s in suites: s.tests = [] for t in test_backup[s]: max_percent = test_results.get(t.path, 0) if not max_percent or max_percent < 1.0: continue max_percent = int(max_percent) subtests_count = self._calculate_n_tests(max_percent, options) if options.verbose: print ('%s [x%d] (max marking limit=%.02f)' % (t.path, subtests_count, max_percent)) for i in xrange(0, subtests_count): fuzzer_seed = self._next_fuzzer_seed() fuzzing_flags = [ '--stress_marking', str(max_percent), '--fuzzer_random_seed', str(fuzzer_seed), ] if options.stress_compaction: fuzzing_flags.append('--stress_compaction_random') s.tests.append(t.create_variant(t.variant, fuzzing_flags, i)) for t in s.tests: t.cmd = t.get_command(ctx) num_tests += len(s.tests) if num_tests == 0: print "No tests to run." return exit_code print(">>> Fuzzing phase (%d test cases)" % num_tests) progress_indicator = progress.PROGRESS_INDICATORS[options.progress]() runner = execution.Runner(suites, progress_indicator, ctx) return runner.Run(options.j) or exit_code def _load_tests(self, args, options, suites, ctx): # 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": False, "gc_fuzzer": True, "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 for s in suites: s.ReadStatusFile(variables) s.ReadTestCases(ctx) if len(args) > 0: s.FilterTestCasesByArgs(args) s.FilterTestCasesByStatus(False) num_tests += len(s.tests) for t in s.tests: t.id = test_id test_id += 1 return num_tests # Parses test stdout and returns what was the highest reached percent of the # incremental marking limit (0-100). @staticmethod def _get_max_limit_reached(output): if not output.stdout: return None for l in reversed(output.stdout.splitlines()): if l.startswith('### Maximum marking limit reached ='): return float(l.split()[6]) return None def _next_fuzzer_seed(self): fuzzer_seed = None while not fuzzer_seed: fuzzer_seed = self.fuzzer_rng.randint(-2147483648, 2147483647) return fuzzer_seed if __name__ == '__main__': sys.exit(GCFuzzer().execute())