| Index: telemetry/telemetry/testing/run_browser_tests.py
|
| diff --git a/telemetry/telemetry/testing/run_browser_tests.py b/telemetry/telemetry/testing/run_browser_tests.py
|
| deleted file mode 100644
|
| index 6ce835aa1c583d4623b4735fd9abfe31ffe59a62..0000000000000000000000000000000000000000
|
| --- a/telemetry/telemetry/testing/run_browser_tests.py
|
| +++ /dev/null
|
| @@ -1,357 +0,0 @@
|
| -# Copyright 2016 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -import fnmatch
|
| -import re
|
| -import sys
|
| -import json
|
| -
|
| -from telemetry.core import discover
|
| -from telemetry.internal.browser import browser_options
|
| -from telemetry.internal.platform import android_device
|
| -from telemetry.internal.util import binary_manager
|
| -from telemetry.testing import browser_test_context
|
| -from telemetry.testing import serially_executed_browser_test_case
|
| -
|
| -import typ
|
| -from typ import arg_parser
|
| -
|
| -DEFAULT_LOG_FORMAT = (
|
| - '(%(levelname)s) %(asctime)s %(module)s.%(funcName)s:%(lineno)d '
|
| - '%(message)s')
|
| -
|
| -
|
| -TEST_SUFFIXES = ['*_test.py', '*_tests.py', '*_unittest.py', '*_unittests.py']
|
| -
|
| -
|
| -def ProcessCommandLineOptions(test_class, default_chrome_root, args):
|
| - options = browser_options.BrowserFinderOptions()
|
| - options.browser_type = 'any'
|
| - parser = options.CreateParser(test_class.__doc__)
|
| - test_class.AddCommandlineArgs(parser)
|
| - # Set the default chrome root variable. This is required for the
|
| - # Android browser finder to function properly.
|
| - if default_chrome_root:
|
| - parser.set_defaults(chrome_root=default_chrome_root)
|
| - finder_options, positional_args = parser.parse_args(args)
|
| - finder_options.positional_args = positional_args
|
| - return finder_options
|
| -
|
| -
|
| -def _ValidateDistinctNames(browser_test_classes):
|
| - names_to_test_classes = {}
|
| - for cl in browser_test_classes:
|
| - name = cl.Name()
|
| - if name in names_to_test_classes:
|
| - raise Exception('Test name %s is duplicated between %s and %s' % (
|
| - name, repr(cl), repr(names_to_test_classes[name])))
|
| - names_to_test_classes[name] = cl
|
| -
|
| -
|
| -def _TestRangeForShard(total_shards, shard_index, num_tests):
|
| - """Returns a 2-tuple containing the start (inclusive) and ending
|
| - (exclusive) indices of the tests that should be run, given that
|
| - |num_tests| tests are split across |total_shards| shards, and that
|
| - |shard_index| is currently being run.
|
| - """
|
| - assert num_tests >= 0
|
| - assert total_shards >= 1
|
| - assert shard_index >= 0 and shard_index < total_shards, (
|
| - 'shard_index (%d) must be >= 0 and < total_shards (%d)' %
|
| - (shard_index, total_shards))
|
| - if num_tests == 0:
|
| - return (0, 0)
|
| - floored_tests_per_shard = num_tests // total_shards
|
| - remaining_tests = num_tests % total_shards
|
| - if remaining_tests == 0:
|
| - return (floored_tests_per_shard * shard_index,
|
| - floored_tests_per_shard * (1 + shard_index))
|
| - # More complicated. Some shards will run floored_tests_per_shard
|
| - # tests, and some will run 1 + floored_tests_per_shard.
|
| - num_earlier_shards_with_one_extra_test = min(remaining_tests, shard_index)
|
| - num_earlier_shards_with_no_extra_tests = max(
|
| - 0, shard_index - num_earlier_shards_with_one_extra_test)
|
| - num_earlier_tests = (
|
| - num_earlier_shards_with_one_extra_test * (floored_tests_per_shard + 1) +
|
| - num_earlier_shards_with_no_extra_tests * floored_tests_per_shard)
|
| - tests_for_this_shard = floored_tests_per_shard
|
| - if shard_index < remaining_tests:
|
| - tests_for_this_shard += 1
|
| - return (num_earlier_tests, num_earlier_tests + tests_for_this_shard)
|
| -
|
| -
|
| -def _MedianTestTime(test_times):
|
| - times = test_times.values()
|
| - times.sort()
|
| - if len(times) == 0:
|
| - return 0
|
| - halfLen = len(times) / 2
|
| - if len(times) % 2:
|
| - return times[halfLen]
|
| - else:
|
| - return 0.5 * (times[halfLen - 1] + times[halfLen])
|
| -
|
| -
|
| -def _TestTime(test, test_times, default_test_time):
|
| - return test_times.get(test.shortName()) or default_test_time
|
| -
|
| -
|
| -def _DebugShardDistributions(shards, test_times):
|
| - for i, s in enumerate(shards):
|
| - num_tests = len(s)
|
| - if test_times:
|
| - median = _MedianTestTime(test_times)
|
| - shard_time = 0.0
|
| - for t in s:
|
| - shard_time += _TestTime(t, test_times, median)
|
| - print 'shard %d: %d seconds (%d tests)' % (i, shard_time, num_tests)
|
| - else:
|
| - print 'shard %d: %d tests (unknown duration)' % (i, num_tests)
|
| -
|
| -
|
| -def _SplitShardsByTime(test_cases, total_shards, test_times,
|
| - debug_shard_distributions):
|
| - median = _MedianTestTime(test_times)
|
| - shards = []
|
| - for i in xrange(total_shards):
|
| - shards.append({'total_time': 0.0, 'tests': []})
|
| - test_cases.sort(key=lambda t: _TestTime(t, test_times, median),
|
| - reverse=True)
|
| -
|
| - # The greedy algorithm has been empirically tested on the WebGL 2.0
|
| - # conformance tests' times, and results in an essentially perfect
|
| - # shard distribution of 530 seconds per shard. In the same scenario,
|
| - # round-robin scheduling resulted in shard times spread between 502
|
| - # and 592 seconds, and the current alphabetical sharding resulted in
|
| - # shard times spread between 44 and 1591 seconds.
|
| -
|
| - # Greedy scheduling. O(m*n), where m is the number of shards and n
|
| - # is the number of test cases.
|
| - for t in test_cases:
|
| - min_shard_index = 0
|
| - min_shard_time = None
|
| - for i in xrange(total_shards):
|
| - if min_shard_time is None or shards[i]['total_time'] < min_shard_time:
|
| - min_shard_index = i
|
| - min_shard_time = shards[i]['total_time']
|
| - shards[min_shard_index]['tests'].append(t)
|
| - shards[min_shard_index]['total_time'] += _TestTime(t, test_times, median)
|
| -
|
| - res = [s['tests'] for s in shards]
|
| - if debug_shard_distributions:
|
| - _DebugShardDistributions(res, test_times)
|
| -
|
| - return res
|
| -
|
| -
|
| -def LoadTestCasesToBeRun(
|
| - test_class, finder_options, filter_regex_str, filter_tests_after_sharding,
|
| - total_shards, shard_index, test_times, debug_shard_distributions):
|
| - test_cases = []
|
| - real_regex = re.compile(filter_regex_str)
|
| - noop_regex = re.compile('')
|
| - if filter_tests_after_sharding:
|
| - filter_regex = noop_regex
|
| - post_filter_regex = real_regex
|
| - else:
|
| - filter_regex = real_regex
|
| - post_filter_regex = noop_regex
|
| -
|
| - for t in serially_executed_browser_test_case.GenerateTestCases(
|
| - test_class, finder_options):
|
| - if filter_regex.search(t.shortName()):
|
| - test_cases.append(t)
|
| -
|
| - if test_times:
|
| - # Assign tests to shards.
|
| - shards = _SplitShardsByTime(test_cases, total_shards, test_times,
|
| - debug_shard_distributions)
|
| - return [t for t in shards[shard_index]
|
| - if post_filter_regex.search(t.shortName())]
|
| - else:
|
| - test_cases.sort(key=lambda t: t.shortName())
|
| - test_range = _TestRangeForShard(total_shards, shard_index, len(test_cases))
|
| - if debug_shard_distributions:
|
| - tmp_shards = []
|
| - for i in xrange(total_shards):
|
| - tmp_range = _TestRangeForShard(total_shards, i, len(test_cases))
|
| - tmp_shards.append(test_cases[tmp_range[0]:tmp_range[1]])
|
| - # Can edit the code to get 'test_times' passed in here for
|
| - # debugging and comparison purposes.
|
| - _DebugShardDistributions(tmp_shards, None)
|
| - return [t for t in test_cases[test_range[0]:test_range[1]]
|
| - if post_filter_regex.search(t.shortName())]
|
| -
|
| -
|
| -def _CreateTestArgParsers():
|
| - parser = typ.ArgumentParser(discovery=False, reporting=True, running=True)
|
| - parser.add_argument('test', type=str, help='Name of the test suite to run')
|
| - parser.add_argument('--test-filter', type=str, default='', action='store',
|
| - help='Run only tests whose names match the given filter regexp.')
|
| - parser.add_argument(
|
| - '--filter-tests-after-sharding', default=False, action='store_true',
|
| - help=('Apply the test filter after tests are split for sharding. Useful '
|
| - 'for reproducing bugs related to the order in which tests run.'))
|
| - parser.add_argument(
|
| - '--read-abbreviated-json-results-from', metavar='FILENAME',
|
| - action='store', help=(
|
| - 'If specified, reads abbreviated results from that path in json form. '
|
| - 'This information is used to more evenly distribute tests among '
|
| - 'shards.'))
|
| - parser.add_argument('--debug-shard-distributions',
|
| - action='store_true', default=False,
|
| - help='Print debugging information about the shards\' test distributions')
|
| -
|
| - parser.add_argument('--default-chrome-root', type=str, default=None)
|
| - parser.add_argument('--client-config', dest='client_configs',
|
| - action='append', default=[])
|
| - parser.add_argument('--start-dir', dest='start_dirs',
|
| - action='append', default=[])
|
| - parser.add_argument('--skip', metavar='glob', default=[],
|
| - action='append',
|
| - help=('Globs of test names to skip (defaults to %(default)s).'))
|
| - return parser
|
| -
|
| -
|
| -def _SkipMatch(name, skipGlobs):
|
| - return any(fnmatch.fnmatch(name, glob) for glob in skipGlobs)
|
| -
|
| -
|
| -def _GetClassifier(args):
|
| - def _SeriallyExecutedBrowserTestCaseClassifer(test_set, test):
|
| - # Do not pick up tests that do not inherit from
|
| - # serially_executed_browser_test_case.SeriallyExecutedBrowserTestCase
|
| - # class.
|
| - if not isinstance(test,
|
| - serially_executed_browser_test_case.SeriallyExecutedBrowserTestCase):
|
| - return
|
| - name = test.id()
|
| - if _SkipMatch(name, args.skip):
|
| - test_set.tests_to_skip.append(
|
| - typ.TestInput(name, 'skipped because matched --skip'))
|
| - return
|
| - # For now, only support running these tests serially.
|
| - test_set.isolated_tests.append(typ.TestInput(name))
|
| - return _SeriallyExecutedBrowserTestCaseClassifer
|
| -
|
| -
|
| -def RunTests(args):
|
| - parser = _CreateTestArgParsers()
|
| - try:
|
| - options, extra_args = parser.parse_known_args(args)
|
| - except arg_parser._Bailout:
|
| - return parser.exit_status
|
| - binary_manager.InitDependencyManager(options.client_configs)
|
| -
|
| - for start_dir in options.start_dirs:
|
| - modules_to_classes = discover.DiscoverClasses(
|
| - start_dir, options.top_level_dir,
|
| - base_class=serially_executed_browser_test_case.
|
| - SeriallyExecutedBrowserTestCase)
|
| - browser_test_classes = modules_to_classes.values()
|
| -
|
| - _ValidateDistinctNames(browser_test_classes)
|
| -
|
| - test_class = None
|
| - for cl in browser_test_classes:
|
| - if cl.Name() == options.test:
|
| - test_class = cl
|
| - break
|
| -
|
| - if not test_class:
|
| - print 'Cannot find test class with name matching %s' % options.test
|
| - print 'Available tests: %s' % '\n'.join(
|
| - cl.Name() for cl in browser_test_classes)
|
| - return 1
|
| -
|
| - # Create test context.
|
| - context = browser_test_context.TypTestContext()
|
| - for c in options.client_configs:
|
| - context.client_configs.append(c)
|
| - context.finder_options = ProcessCommandLineOptions(
|
| - test_class, options.default_chrome_root, extra_args)
|
| - context.test_class = test_class
|
| - test_times = None
|
| - if options.read_abbreviated_json_results_from:
|
| - with open(options.read_abbreviated_json_results_from, 'r') as f:
|
| - abbr_results = json.load(f)
|
| - test_times = abbr_results.get('times')
|
| - tests_to_run = LoadTestCasesToBeRun(
|
| - test_class=test_class, finder_options=context.finder_options,
|
| - filter_regex_str=options.test_filter,
|
| - filter_tests_after_sharding=options.filter_tests_after_sharding,
|
| - total_shards=options.total_shards, shard_index=options.shard_index,
|
| - test_times=test_times,
|
| - debug_shard_distributions=options.debug_shard_distributions)
|
| - for t in tests_to_run:
|
| - context.test_case_ids_to_run.add(t.id())
|
| - context.Freeze()
|
| - browser_test_context._global_test_context = context
|
| -
|
| - # Setup typ runner.
|
| - runner = typ.Runner()
|
| -
|
| - runner.context = context
|
| - runner.setup_fn = _SetUpProcess
|
| - runner.teardown_fn = _TearDownProcess
|
| -
|
| - runner.args.jobs = options.jobs
|
| - runner.args.metadata = options.metadata
|
| - runner.args.passthrough = options.passthrough
|
| - runner.args.path = options.path
|
| - runner.args.retry_limit = options.retry_limit
|
| - runner.args.test_results_server = options.test_results_server
|
| - runner.args.test_type = options.test_type
|
| - runner.args.top_level_dir = options.top_level_dir
|
| - runner.args.write_full_results_to = options.write_full_results_to
|
| - runner.args.write_trace_to = options.write_trace_to
|
| - runner.args.list_only = options.list_only
|
| - runner.classifier = _GetClassifier(options)
|
| -
|
| - runner.args.suffixes = TEST_SUFFIXES
|
| -
|
| - # Since sharding logic is handled by browser_test_runner harness by passing
|
| - # browser_test_context.test_case_ids_to_run to subprocess to indicate test
|
| - # cases to be run, we explicitly disable sharding logic in typ.
|
| - runner.args.total_shards = 1
|
| - runner.args.shard_index = 0
|
| -
|
| - runner.args.timing = True
|
| - runner.args.verbose = options.verbose
|
| - runner.win_multiprocessing = typ.WinMultiprocessing.importable
|
| - try:
|
| - ret, _, _ = runner.run()
|
| - except KeyboardInterrupt:
|
| - print >> sys.stderr, "interrupted, exiting"
|
| - ret = 130
|
| - return ret
|
| -
|
| -
|
| -def _SetUpProcess(child, context):
|
| - del child # Unused.
|
| - args = context.finder_options
|
| - if binary_manager.NeedsInit():
|
| - # On windows, typ doesn't keep the DependencyManager initialization in the
|
| - # child processes.
|
| - binary_manager.InitDependencyManager(context.client_configs)
|
| - if args.remote_platform_options.device == 'android':
|
| - android_devices = android_device.FindAllAvailableDevices(args)
|
| - if not android_devices:
|
| - raise RuntimeError("No Android device found")
|
| - android_devices.sort(key=lambda device: device.name)
|
| - args.remote_platform_options.device = (
|
| - android_devices[child.worker_num-1].guid)
|
| - browser_test_context._global_test_context = context
|
| - context.test_class.SetUpProcess()
|
| -
|
| -
|
| -def _TearDownProcess(child, context):
|
| - del child, context # Unused.
|
| - browser_test_context._global_test_context.test_class.TearDownProcess()
|
| - browser_test_context._global_test_context = None
|
| -
|
| -
|
| -if __name__ == '__main__':
|
| - ret_code = RunTests(sys.argv[1:])
|
| - sys.exit(ret_code)
|
|
|