| # 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 collections import OrderedDict |
| import json |
| import optparse |
| import os |
| import sys |
| |
| |
| # Add testrunner to the path. |
| sys.path.insert( |
| 0, |
| os.path.dirname( |
| os.path.dirname(os.path.abspath(__file__)))) |
| |
| |
| from local import testsuite |
| from local import utils |
| |
| from testproc.shard import ShardProc |
| |
| |
| BASE_DIR = ( |
| os.path.dirname( |
| os.path.dirname( |
| os.path.dirname( |
| os.path.abspath(__file__))))) |
| |
| DEFAULT_OUT_GN = 'out.gn' |
| |
| ARCH_GUESS = utils.DefaultArch() |
| |
| # Map of test name synonyms to lists of test suites. Should be ordered by |
| # expected runtimes (suites with slow test cases first). These groups are |
| # invoked in separate steps on the bots. |
| TEST_MAP = { |
| # This needs to stay in sync with test/bot_default.isolate. |
| "bot_default": [ |
| "debugger", |
| "mjsunit", |
| "cctest", |
| "wasm-spec-tests", |
| "inspector", |
| "webkit", |
| "mkgrokdump", |
| "fuzzer", |
| "message", |
| "preparser", |
| "intl", |
| "unittests", |
| ], |
| # This needs to stay in sync with test/default.isolate. |
| "default": [ |
| "debugger", |
| "mjsunit", |
| "cctest", |
| "wasm-spec-tests", |
| "inspector", |
| "mkgrokdump", |
| "fuzzer", |
| "message", |
| "preparser", |
| "intl", |
| "unittests", |
| ], |
| # This needs to stay in sync with test/d8_default.isolate. |
| "d8_default": [ |
| # TODO(machenbach): uncomment after infra side lands. |
| #"debugger", |
| "mjsunit", |
| "webkit", |
| #"message", |
| #"preparser", |
| #"intl", |
| ], |
| # This needs to stay in sync with test/optimize_for_size.isolate. |
| "optimize_for_size": [ |
| "debugger", |
| "mjsunit", |
| "cctest", |
| "inspector", |
| "webkit", |
| "intl", |
| ], |
| "unittests": [ |
| "unittests", |
| ], |
| } |
| |
| |
| class ModeConfig(object): |
| def __init__(self, flags, timeout_scalefactor, status_mode, execution_mode): |
| self.flags = flags |
| self.timeout_scalefactor = timeout_scalefactor |
| self.status_mode = status_mode |
| self.execution_mode = execution_mode |
| |
| |
| DEBUG_FLAGS = ["--nohard-abort", "--enable-slow-asserts", "--verify-heap"] |
| RELEASE_FLAGS = ["--nohard-abort"] |
| MODES = { |
| "debug": ModeConfig( |
| flags=DEBUG_FLAGS, |
| timeout_scalefactor=4, |
| status_mode="debug", |
| execution_mode="debug", |
| ), |
| "optdebug": ModeConfig( |
| flags=DEBUG_FLAGS, |
| timeout_scalefactor=4, |
| status_mode="debug", |
| execution_mode="debug", |
| ), |
| "release": ModeConfig( |
| flags=RELEASE_FLAGS, |
| timeout_scalefactor=1, |
| status_mode="release", |
| execution_mode="release", |
| ), |
| # Normal trybot release configuration. There, dchecks are always on which |
| # implies debug is set. Hence, the status file needs to assume debug-like |
| # behavior/timeouts. |
| "tryrelease": ModeConfig( |
| flags=RELEASE_FLAGS, |
| timeout_scalefactor=1, |
| status_mode="debug", |
| execution_mode="release", |
| ), |
| # This mode requires v8 to be compiled with dchecks and slow dchecks. |
| "slowrelease": ModeConfig( |
| flags=RELEASE_FLAGS + ["--enable-slow-asserts"], |
| timeout_scalefactor=2, |
| status_mode="debug", |
| execution_mode="release", |
| ), |
| } |
| |
| |
| class TestRunnerError(Exception): |
| pass |
| |
| |
| class BuildConfig(object): |
| def __init__(self, build_config): |
| # In V8 land, GN's x86 is called ia32. |
| if build_config['v8_target_cpu'] == 'x86': |
| self.arch = 'ia32' |
| else: |
| self.arch = build_config['v8_target_cpu'] |
| |
| self.is_debug = build_config['is_debug'] |
| self.asan = build_config['is_asan'] |
| self.cfi_vptr = build_config['is_cfi'] |
| self.dcheck_always_on = build_config['dcheck_always_on'] |
| self.gcov_coverage = build_config['is_gcov_coverage'] |
| self.msan = build_config['is_msan'] |
| self.no_i18n = not build_config['v8_enable_i18n_support'] |
| self.no_snap = not build_config['v8_use_snapshot'] |
| self.predictable = build_config['v8_enable_verify_predictable'] |
| self.tsan = build_config['is_tsan'] |
| self.ubsan_vptr = build_config['is_ubsan_vptr'] |
| |
| def __str__(self): |
| detected_options = [] |
| |
| if self.asan: |
| detected_options.append('asan') |
| if self.cfi_vptr: |
| detected_options.append('cfi_vptr') |
| if self.dcheck_always_on: |
| detected_options.append('dcheck_always_on') |
| if self.gcov_coverage: |
| detected_options.append('gcov_coverage') |
| if self.msan: |
| detected_options.append('msan') |
| if self.no_i18n: |
| detected_options.append('no_i18n') |
| if self.no_snap: |
| detected_options.append('no_snap') |
| if self.predictable: |
| detected_options.append('predictable') |
| if self.tsan: |
| detected_options.append('tsan') |
| if self.ubsan_vptr: |
| detected_options.append('ubsan_vptr') |
| |
| return '\n'.join(detected_options) |
| |
| |
| class BaseTestRunner(object): |
| def __init__(self, basedir=None): |
| self.basedir = basedir or BASE_DIR |
| self.outdir = None |
| self.build_config = None |
| self.mode_name = None |
| self.mode_options = None |
| |
| def execute(self, sys_args=None): |
| if sys_args is None: # pragma: no cover |
| sys_args = sys.argv[1:] |
| try: |
| parser = self._create_parser() |
| options, args = self._parse_args(parser, sys_args) |
| |
| self._load_build_config(options) |
| |
| try: |
| self._process_default_options(options) |
| self._process_options(options) |
| except TestRunnerError: |
| parser.print_help() |
| raise |
| |
| args = self._parse_test_args(args) |
| suites = self._get_suites(args, options.verbose) |
| |
| self._setup_env() |
| return self._do_execute(suites, args, options) |
| except TestRunnerError: |
| return 1 |
| except KeyboardInterrupt: |
| return 2 |
| |
| def _create_parser(self): |
| parser = optparse.OptionParser() |
| parser.usage = '%prog [options] [tests]' |
| parser.description = """TESTS: %s""" % (TEST_MAP["default"]) |
| self._add_parser_default_options(parser) |
| self._add_parser_options(parser) |
| return parser |
| |
| def _add_parser_default_options(self, parser): |
| parser.add_option("--gn", help="Scan out.gn for the last built" |
| " configuration", |
| default=False, action="store_true") |
| parser.add_option("--outdir", help="Base directory with compile output", |
| default="out") |
| parser.add_option("--buildbot", help="DEPRECATED!", |
| default=False, action="store_true") |
| parser.add_option("--arch", |
| help="The architecture to run tests for") |
| parser.add_option("-m", "--mode", |
| help="The test mode in which to run (uppercase for ninja" |
| " and buildbot builds): %s" % MODES.keys()) |
| parser.add_option("--shell-dir", help="DEPRECATED! Executables from build " |
| "directory will be used") |
| parser.add_option("-v", "--verbose", help="Verbose output", |
| default=False, action="store_true") |
| parser.add_option("--shard-count", |
| help="Split tests into this number of shards", |
| default=1, type="int") |
| parser.add_option("--shard-run", |
| help="Run this shard from the split up tests.", |
| default=1, type="int") |
| |
| def _add_parser_options(self, parser): |
| pass |
| |
| def _parse_args(self, parser, sys_args): |
| options, args = parser.parse_args(sys_args) |
| |
| if any(map(lambda v: v and ',' in v, |
| [options.arch, options.mode])): # pragma: no cover |
| print 'Multiple arch/mode are deprecated' |
| raise TestRunnerError() |
| |
| return options, args |
| |
| def _load_build_config(self, options): |
| for outdir in self._possible_outdirs(options): |
| try: |
| self.build_config = self._do_load_build_config(outdir, options.verbose) |
| except TestRunnerError: |
| pass |
| |
| if not self.build_config: # pragma: no cover |
| print 'Failed to load build config' |
| raise TestRunnerError |
| |
| print 'Build found: %s' % self.outdir |
| if str(self.build_config): |
| print '>>> Autodetected:' |
| print self.build_config |
| |
| # Returns possible build paths in order: |
| # gn |
| # outdir |
| # outdir/arch.mode |
| # Each path is provided in two versions: <path> and <path>/mode for buildbot. |
| def _possible_outdirs(self, options): |
| def outdirs(): |
| if options.gn: |
| yield self._get_gn_outdir() |
| return |
| |
| yield options.outdir |
| if options.arch and options.mode: |
| yield os.path.join(options.outdir, |
| '%s.%s' % (options.arch, options.mode)) |
| |
| for outdir in outdirs(): |
| yield os.path.join(self.basedir, outdir) |
| |
| # buildbot option |
| if options.mode: |
| yield os.path.join(self.basedir, outdir, options.mode) |
| |
| def _get_gn_outdir(self): |
| gn_out_dir = os.path.join(self.basedir, DEFAULT_OUT_GN) |
| latest_timestamp = -1 |
| latest_config = None |
| for gn_config in os.listdir(gn_out_dir): |
| gn_config_dir = os.path.join(gn_out_dir, gn_config) |
| if not os.path.isdir(gn_config_dir): |
| continue |
| if os.path.getmtime(gn_config_dir) > latest_timestamp: |
| latest_timestamp = os.path.getmtime(gn_config_dir) |
| latest_config = gn_config |
| if latest_config: |
| print(">>> Latest GN build found: %s" % latest_config) |
| return os.path.join(DEFAULT_OUT_GN, latest_config) |
| |
| def _do_load_build_config(self, outdir, verbose=False): |
| build_config_path = os.path.join(outdir, "v8_build_config.json") |
| if not os.path.exists(build_config_path): |
| if verbose: |
| print("Didn't find build config: %s" % build_config_path) |
| raise TestRunnerError() |
| |
| with open(build_config_path) as f: |
| try: |
| build_config_json = json.load(f) |
| except Exception: # pragma: no cover |
| print("%s exists but contains invalid json. Is your build up-to-date?" |
| % build_config_path) |
| raise TestRunnerError() |
| |
| # In auto-detect mode the outdir is always where we found the build config. |
| # This ensures that we'll also take the build products from there. |
| self.outdir = os.path.dirname(build_config_path) |
| |
| return BuildConfig(build_config_json) |
| |
| def _process_default_options(self, options): |
| # We don't use the mode for more path-magic. |
| # Therefore transform the buildbot mode here to fix build_config value. |
| if options.mode: |
| options.mode = self._buildbot_to_v8_mode(options.mode) |
| |
| build_config_mode = 'debug' if self.build_config.is_debug else 'release' |
| if options.mode: |
| if options.mode not in MODES: # pragma: no cover |
| print '%s mode is invalid' % options.mode |
| raise TestRunnerError() |
| if MODES[options.mode].execution_mode != build_config_mode: |
| print ('execution mode (%s) for %s is inconsistent with build config ' |
| '(%s)' % ( |
| MODES[options.mode].execution_mode, |
| options.mode, |
| build_config_mode)) |
| raise TestRunnerError() |
| |
| self.mode_name = options.mode |
| else: |
| self.mode_name = build_config_mode |
| |
| self.mode_options = MODES[self.mode_name] |
| |
| if options.arch and options.arch != self.build_config.arch: |
| print('--arch value (%s) inconsistent with build config (%s).' % ( |
| options.arch, self.build_config.arch)) |
| raise TestRunnerError() |
| |
| if options.shell_dir: # pragma: no cover |
| print('Warning: --shell-dir is deprecated. Searching for executables in ' |
| 'build directory (%s) instead.' % self.outdir) |
| |
| def _buildbot_to_v8_mode(self, config): |
| """Convert buildbot build configs to configs understood by the v8 runner. |
| |
| V8 configs are always lower case and without the additional _x64 suffix |
| for 64 bit builds on windows with ninja. |
| """ |
| mode = config[:-4] if config.endswith('_x64') else config |
| return mode.lower() |
| |
| def _process_options(self, options): |
| pass |
| |
| def _setup_env(self): |
| # Use the v8 root as cwd as some test cases use "load" with relative paths. |
| os.chdir(self.basedir) |
| |
| # Many tests assume an English interface. |
| os.environ['LANG'] = 'en_US.UTF-8' |
| |
| symbolizer_option = self._get_external_symbolizer_option() |
| |
| if self.build_config.asan: |
| asan_options = [ |
| symbolizer_option, |
| 'allow_user_segv_handler=1', |
| 'allocator_may_return_null=1', |
| ] |
| if not utils.GuessOS() in ['macos', 'windows']: |
| # LSAN is not available on mac and windows. |
| asan_options.append('detect_leaks=1') |
| else: |
| asan_options.append('detect_leaks=0') |
| os.environ['ASAN_OPTIONS'] = ":".join(asan_options) |
| |
| if self.build_config.cfi_vptr: |
| os.environ['UBSAN_OPTIONS'] = ":".join([ |
| 'print_stacktrace=1', |
| 'print_summary=1', |
| 'symbolize=1', |
| symbolizer_option, |
| ]) |
| |
| if self.build_config.ubsan_vptr: |
| os.environ['UBSAN_OPTIONS'] = ":".join([ |
| 'print_stacktrace=1', |
| symbolizer_option, |
| ]) |
| |
| if self.build_config.msan: |
| os.environ['MSAN_OPTIONS'] = symbolizer_option |
| |
| if self.build_config.tsan: |
| suppressions_file = os.path.join( |
| self.basedir, |
| 'tools', |
| 'sanitizers', |
| 'tsan_suppressions.txt') |
| os.environ['TSAN_OPTIONS'] = " ".join([ |
| symbolizer_option, |
| 'suppressions=%s' % suppressions_file, |
| 'exit_code=0', |
| 'report_thread_leaks=0', |
| 'history_size=7', |
| 'report_destroy_locked=0', |
| ]) |
| |
| def _get_external_symbolizer_option(self): |
| external_symbolizer_path = os.path.join( |
| self.basedir, |
| 'third_party', |
| 'llvm-build', |
| 'Release+Asserts', |
| 'bin', |
| 'llvm-symbolizer', |
| ) |
| |
| if utils.IsWindows(): |
| # Quote, because sanitizers might confuse colon as option separator. |
| external_symbolizer_path = '"%s.exe"' % external_symbolizer_path |
| |
| return 'external_symbolizer_path=%s' % external_symbolizer_path |
| |
| def _parse_test_args(self, args): |
| if not args: |
| args = self._get_default_suite_names() |
| |
| # Expand arguments with grouped tests. The args should reflect the list |
| # of suites as otherwise filters would break. |
| def expand_test_group(name): |
| return TEST_MAP.get(name, [name]) |
| |
| return reduce(list.__add__, map(expand_test_group, args), []) |
| |
| def _get_suites(self, args, verbose=False): |
| names = self._args_to_suite_names(args) |
| return self._load_suites(names, verbose) |
| |
| def _args_to_suite_names(self, args): |
| # Use default tests if no test configuration was provided at the cmd line. |
| all_names = set(utils.GetSuitePaths(os.path.join(self.basedir, 'test'))) |
| args_names = OrderedDict([(arg.split('/')[0], None) for arg in args]) # set |
| return [name for name in args_names if name in all_names] |
| |
| def _get_default_suite_names(self): |
| return [] |
| |
| def _expand_test_group(self, name): |
| return TEST_MAP.get(name, [name]) |
| |
| def _load_suites(self, names, verbose=False): |
| def load_suite(name): |
| if verbose: |
| print '>>> Loading test suite: %s' % name |
| return testsuite.TestSuite.LoadTestSuite( |
| os.path.join(self.basedir, 'test', name)) |
| return map(load_suite, names) |
| |
| # TODO(majeski): remove options & args parameters |
| def _do_execute(self, suites, args, options): |
| raise NotImplementedError() |
| |
| def _create_shard_proc(self, options): |
| myid, count = self._get_shard_info(options) |
| if count == 1: |
| return None |
| return ShardProc(myid - 1, count) |
| |
| def _get_shard_info(self, options): |
| """ |
| Returns pair: |
| (id of the current shard [1; number of shards], number of shards) |
| """ |
| # Read gtest shard configuration from environment (e.g. set by swarming). |
| # If none is present, use values passed on the command line. |
| shard_count = int( |
| os.environ.get('GTEST_TOTAL_SHARDS', options.shard_count)) |
| shard_run = os.environ.get('GTEST_SHARD_INDEX') |
| if shard_run is not None: |
| # The v8 shard_run starts at 1, while GTEST_SHARD_INDEX starts at 0. |
| shard_run = int(shard_run) + 1 |
| else: |
| shard_run = options.shard_run |
| |
| if options.shard_count > 1: |
| # Log if a value was passed on the cmd line and it differs from the |
| # environment variables. |
| if options.shard_count != shard_count: # pragma: no cover |
| print("shard_count from cmd line differs from environment variable " |
| "GTEST_TOTAL_SHARDS") |
| if (options.shard_run > 1 and |
| options.shard_run != shard_run): # pragma: no cover |
| print("shard_run from cmd line differs from environment variable " |
| "GTEST_SHARD_INDEX") |
| |
| if shard_run < 1 or shard_run > shard_count: |
| # TODO(machenbach): Turn this into an assert. If that's wrong on the |
| # bots, printing will be quite useless. Or refactor this code to make |
| # sure we get a return code != 0 after testing if we got here. |
| print "shard-run not a valid number, should be in [1:shard-count]" |
| print "defaulting back to running all tests" |
| return 1, 1 |
| |
| return shard_run, shard_count |