| Index: third_party/WebKit/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
|
| diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/tool/commands/rebaseline.py b/third_party/WebKit/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
|
| index 28ad3f65eafaa6666213e0adf15a0ac7f85675d4..d4a67c0d52ce7c68419710f71335ee5d59aa9cb9 100644
|
| --- a/third_party/WebKit/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
|
| +++ b/third_party/WebKit/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
|
| @@ -27,6 +27,7 @@
|
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
| from __future__ import print_function
|
| +import collections
|
| import json
|
| import logging
|
| import optparse
|
| @@ -134,6 +135,50 @@ class ChangeSet(object):
|
| self.lines_to_remove[test].extend(other.lines_to_remove[test])
|
|
|
|
|
| +class TestBaselineSet(object):
|
| + """Represents a collection of tests and platforms that can be rebaselined.
|
| +
|
| + A TestBaselineSet specifies tests to rebaseline along with information
|
| + about where to fetch the baselines from.
|
| + """
|
| +
|
| + def __init__(self, host):
|
| + self._host = host
|
| + self._test_prefix_map = collections.defaultdict(list)
|
| +
|
| + def __iter__(self):
|
| + return iter(self._iter_combinations())
|
| +
|
| + def __bool__(self):
|
| + return bool(self._test_prefix_map)
|
| +
|
| + def _iter_combinations(self):
|
| + """Iterates through (test, build) combinations."""
|
| + port = self._host.port_factory.get()
|
| + for test_prefix, builds in self._test_prefix_map.iteritems():
|
| + for build in builds:
|
| + for test in port.tests([test_prefix]):
|
| + yield (test, build)
|
| +
|
| + def __str__(self):
|
| + if not self._test_prefix_map:
|
| + return '<Empty TestBaselineSet>'
|
| + return '<TestBaselineSet with:\n ' + '\n '.join('%s: %s' % pair for pair in self._iter_combinations()) + '>'
|
| +
|
| + def add(self, test_prefix, build):
|
| + """Adds an entry for baselines to download for some set of tests.
|
| +
|
| + Args:
|
| + test_prefix: This can be a full test path, or directory of tests, or a path with globs.
|
| + build: A Build object. This specifies where to fetch baselines from.
|
| + """
|
| + self._test_prefix_map[test_prefix].append(build)
|
| +
|
| + def all_builders(self):
|
| + """Returns all builder names in in this collection."""
|
| + return sorted({b.builder_name for _, b in self._iter_combinations()})
|
| +
|
| +
|
| class CopyExistingBaselinesInternal(AbstractRebaseliningCommand):
|
| name = 'copy-existing-baselines-internal'
|
| help_text = ('Copy existing baselines down one level in the baseline order to ensure '
|
| @@ -313,8 +358,10 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
| def _run_webkit_patch(self, args, verbose):
|
| try:
|
| verbose_args = ['--verbose'] if verbose else []
|
| - stderr = self._tool.executive.run_command([self._tool.path()] + verbose_args +
|
| - args, cwd=self._tool.git().checkout_root, return_stderr=True)
|
| + stderr = self._tool.executive.run_command(
|
| + [self._tool.path()] + verbose_args + args,
|
| + cwd=self._tool.git().checkout_root,
|
| + return_stderr=True)
|
| for line in stderr.splitlines():
|
| _log.warning(line)
|
| except ScriptError:
|
| @@ -329,70 +376,65 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
| (since we don't save debug versions of baselines).
|
|
|
| Args:
|
| - builders_to_check: List of builder names.
|
| + builders_to_check: A collection of builder names.
|
| +
|
| + Returns:
|
| + A set of builders that we may fetch from, which is a subset
|
| + of the input list.
|
| """
|
| release_builders = set()
|
| debug_builders = set()
|
| - builders_to_fallback_paths = {}
|
| for builder in builders_to_check:
|
| port = self._tool.port_factory.get_from_builder_name(builder)
|
| if port.test_configuration().build_type == 'release':
|
| release_builders.add(builder)
|
| else:
|
| debug_builders.add(builder)
|
| +
|
| + builders_to_fallback_paths = {}
|
| for builder in list(release_builders) + list(debug_builders):
|
| port = self._tool.port_factory.get_from_builder_name(builder)
|
| fallback_path = port.baseline_search_path()
|
| if fallback_path not in builders_to_fallback_paths.values():
|
| builders_to_fallback_paths[builder] = fallback_path
|
| - return builders_to_fallback_paths.keys()
|
|
|
| - @staticmethod
|
| - def _builder_names(builds):
|
| - # TODO(qyearsley): If test_prefix_list dicts are converted to instances
|
| - # of some class, then this could be replaced with a method on that class.
|
| - return [b.builder_name for b in builds]
|
| + return set(builders_to_fallback_paths)
|
|
|
| - def _rebaseline_commands(self, test_prefix_list, options):
|
| + def _rebaseline_commands(self, test_baseline_set, options):
|
| path_to_webkit_patch = self._tool.path()
|
| cwd = self._tool.git().checkout_root
|
| copy_baseline_commands = []
|
| rebaseline_commands = []
|
| lines_to_remove = {}
|
| - port = self._tool.port_factory.get()
|
|
|
| - for test_prefix in test_prefix_list:
|
| - for test in port.tests([test_prefix]):
|
| - builders_to_fetch_from = self._builders_to_fetch_from(self._builder_names(test_prefix_list[test_prefix]))
|
| - for build in sorted(test_prefix_list[test_prefix]):
|
| - builder, build_number = build.builder_name, build.build_number
|
| - if builder not in builders_to_fetch_from:
|
| - break
|
| -
|
| - suffixes = self._suffixes_for_actual_failures(test, build)
|
| - if not suffixes:
|
| - # If we're not going to rebaseline the test because it's passing on this
|
| - # builder, we still want to remove the line from TestExpectations.
|
| - if test not in lines_to_remove:
|
| - lines_to_remove[test] = []
|
| - lines_to_remove[test].append(builder)
|
| - continue
|
| -
|
| - args = ['--suffixes', ','.join(suffixes), '--builder', builder, '--test', test]
|
| -
|
| - if options.verbose:
|
| - args.append('--verbose')
|
| -
|
| - copy_baseline_commands.append(
|
| - tuple([[self._tool.executable, path_to_webkit_patch, 'copy-existing-baselines-internal'] + args, cwd]))
|
| -
|
| - if build_number:
|
| - args.extend(['--build-number', str(build_number)])
|
| - if options.results_directory:
|
| - args.extend(['--results-directory', options.results_directory])
|
| -
|
| - rebaseline_commands.append(
|
| - tuple([[self._tool.executable, path_to_webkit_patch, 'rebaseline-test-internal'] + args, cwd]))
|
| + for test, build in test_baseline_set:
|
| + if build.builder_name not in self._builders_to_fetch_from(test_baseline_set.all_builders()):
|
| + continue
|
| +
|
| + suffixes = self._suffixes_for_actual_failures(test, build)
|
| + if not suffixes:
|
| + # If we're not going to rebaseline the test because it's passing on this
|
| + # builder, we still want to remove the line from TestExpectations.
|
| + if test not in lines_to_remove:
|
| + lines_to_remove[test] = []
|
| + lines_to_remove[test].append(build.builder_name)
|
| + continue
|
| +
|
| + args = ['--suffixes', ','.join(suffixes), '--builder', build.builder_name, '--test', test]
|
| + if options.verbose:
|
| + args.append('--verbose')
|
| +
|
| + copy_command = [self._tool.executable, path_to_webkit_patch, 'copy-existing-baselines-internal'] + args
|
| + copy_baseline_commands.append(tuple([copy_command, cwd]))
|
| +
|
| + if build.build_number:
|
| + args.extend(['--build-number', str(build.build_number)])
|
| + if options.results_directory:
|
| + args.extend(['--results-directory', options.results_directory])
|
| +
|
| + rebaseline_command = [self._tool.executable, path_to_webkit_patch, 'rebaseline-test-internal'] + args
|
| + rebaseline_commands.append(tuple([rebaseline_command, cwd]))
|
| +
|
| return copy_baseline_commands, rebaseline_commands, lines_to_remove
|
|
|
| @staticmethod
|
| @@ -413,28 +455,29 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
| _log.debug('Could not add file based off output "%s"', stdout)
|
| return change_set
|
|
|
| - def _optimize_baselines(self, test_prefix_list, verbose=False):
|
| - optimize_commands = []
|
| - for test in test_prefix_list:
|
| - all_suffixes = set()
|
| - builders_to_fetch_from = self._builders_to_fetch_from(self._builder_names(test_prefix_list[test]))
|
| - for build in sorted(test_prefix_list[test]):
|
| - if build.builder_name not in builders_to_fetch_from:
|
| - break
|
| - all_suffixes.update(self._suffixes_for_actual_failures(test, build))
|
| + def _optimize_baselines(self, test_baseline_set, verbose=False):
|
| + """Returns a list of commands to run in parallel to de-duplicate baselines."""
|
| + tests_to_suffixes = collections.defaultdict(set)
|
| + for test, build in test_baseline_set:
|
| + builders_to_fetch_from = self._builders_to_fetch_from(test_baseline_set.all_builders())
|
| + if build.builder_name not in builders_to_fetch_from:
|
| + continue
|
| + tests_to_suffixes[test].update(self._suffixes_for_actual_failures(test, build))
|
|
|
| + optimize_commands = []
|
| + for test, suffixes in tests_to_suffixes.iteritems():
|
| # No need to optimize baselines for a test with no failures.
|
| - if not all_suffixes:
|
| + if not suffixes:
|
| continue
|
| -
|
| # FIXME: We should propagate the platform options as well.
|
| - cmd_line = ['--suffixes', ','.join(all_suffixes), test]
|
| + args = ['--suffixes', ','.join(suffixes), test]
|
| if verbose:
|
| - cmd_line.append('--verbose')
|
| -
|
| + args.append('--verbose')
|
| path_to_webkit_patch = self._tool.path()
|
| cwd = self._tool.git().checkout_root
|
| - optimize_commands.append(tuple([[self._tool.executable, path_to_webkit_patch, 'optimize-baselines'] + cmd_line, cwd]))
|
| + command = [self._tool.executable, path_to_webkit_patch, 'optimize-baselines'] + args
|
| + optimize_commands.append(tuple([command, cwd]))
|
| +
|
| return optimize_commands
|
|
|
| def _update_expectations_files(self, lines_to_remove):
|
| @@ -484,37 +527,23 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
|
|
| return change_set.lines_to_remove
|
|
|
| - def rebaseline(self, options, test_prefix_list):
|
| + def rebaseline(self, options, test_baseline_set):
|
| """Fetches new baselines and removes related test expectation lines.
|
|
|
| Args:
|
| options: An object with the command line options.
|
| - test_prefix_list: A dict of test names to Build objects for new
|
| - baselines. For example:
|
| - {
|
| - "some/test.html": [Build("builder-1", 412),
|
| - Build("builder-2", 100)]},
|
| - "some/other.html": [Build("builder-1", 412)}
|
| - }
|
| - This would mean that new text baselines should be
|
| - downloaded for "some/test.html" on both builder-1
|
| - (build 412) and builder-2 (build 100), and new text
|
| - baselines should be downloaded for "some/other.html"
|
| - but only from builder-1.
|
| - TODO(qyearsley): Replace test_prefix_list everywhere
|
| - with some sort of class that contains the same data.
|
| + test_baseline_set: A TestBaselineSet instance, which represents
|
| + a set of tests/platform combinations to rebaseline.
|
| """
|
| if self._tool.git().has_working_directory_changes(pathspec=self._layout_tests_dir()):
|
| _log.error('There are uncommitted changes in the layout tests directory; aborting.')
|
| return
|
|
|
| - for test, builds_to_check in sorted(test_prefix_list.items()):
|
| + for test in sorted({t for t, _ in test_baseline_set}):
|
| _log.info('Rebaselining %s', test)
|
| - for build in sorted(builds_to_check):
|
| - _log.debug(' %s', build)
|
|
|
| copy_baseline_commands, rebaseline_commands, extra_lines_to_remove = self._rebaseline_commands(
|
| - test_prefix_list, options)
|
| + test_baseline_set, options)
|
| lines_to_remove = {}
|
|
|
| self._run_in_parallel(copy_baseline_commands)
|
| @@ -533,9 +562,9 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
| # TODO(wkorman): Consider changing temporary branch to base off of HEAD rather than
|
| # origin/master to ensure we run baseline optimization processes with the same code as
|
| # auto-rebaseline itself.
|
| - self._run_in_parallel(self._optimize_baselines(test_prefix_list, options.verbose))
|
| + self._run_in_parallel(self._optimize_baselines(test_baseline_set, options.verbose))
|
|
|
| - self._remove_all_pass_testharness_baselines(test_prefix_list)
|
| + self._remove_all_pass_testharness_baselines(test_baseline_set)
|
|
|
| self._tool.git().add_list(self.unstaged_baselines())
|
|
|
| @@ -545,7 +574,7 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
| unstaged_changes = self._tool.git().unstaged_changes()
|
| return sorted(self._tool.git().absolute_path(path) for path in unstaged_changes if re.match(baseline_re, path))
|
|
|
| - def _remove_all_pass_testharness_baselines(self, test_prefix_list):
|
| + def _remove_all_pass_testharness_baselines(self, test_baseline_set):
|
| """Removes all of the all-PASS baselines for the given builders and tests.
|
|
|
| In general, for testharness.js tests, the absence of a baseline
|
| @@ -553,49 +582,31 @@ class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
|
| new all-PASS baselines may be downloaded, but they should not be kept.
|
| """
|
| filesystem = self._tool.filesystem
|
| - baseline_paths = self._all_baseline_paths(test_prefix_list)
|
| + baseline_paths = self._possible_baseline_paths(test_baseline_set)
|
| for path in baseline_paths:
|
| - if not (filesystem.exists(path) and
|
| - filesystem.splitext(path)[1] == '.txt'):
|
| + if not (filesystem.exists(path) and filesystem.splitext(path)[1] == '.txt'):
|
| continue
|
| contents = filesystem.read_text_file(path)
|
| if is_all_pass_testharness_result(contents):
|
| _log.info('Removing all-PASS testharness baseline: %s', path)
|
| filesystem.remove(path)
|
|
|
| - def _all_baseline_paths(self, test_prefix_list):
|
| + def _possible_baseline_paths(self, test_baseline_set):
|
| """Returns file paths for all baselines for the given tests and builders.
|
|
|
| Args:
|
| - test_prefix_list: A dict mapping test prefixes, which could be
|
| - directories or full test paths, to Builds.
|
| - TODO(qyearsley): If a class is added to replace
|
| - test_prefix_list, then this can be made a method on
|
| - that class.
|
| + test_baseline_set: A TestBaselineSet instance.
|
|
|
| Returns:
|
| - A list of absolute paths to possible baseline files,
|
| - which may or may not exist on the local filesystem.
|
| + A list of absolute paths where baselines could possibly exist.
|
| """
|
| filesystem = self._tool.filesystem
|
| - baseline_paths = []
|
| - port = self._tool.port_factory.get()
|
| -
|
| - for test_prefix in test_prefix_list:
|
| - tests = port.tests([test_prefix])
|
| -
|
| - for build in test_prefix_list[test_prefix]:
|
| - port_baseline_dir = self._baseline_directory(build.builder_name)
|
| - baseline_paths.extend([
|
| - filesystem.join(port_baseline_dir, self._file_name_for_expected_result(test, suffix))
|
| - for test in tests for suffix in BASELINE_SUFFIX_LIST
|
| - ])
|
| -
|
| - baseline_paths.extend([
|
| - filesystem.join(self._layout_tests_dir(), self._file_name_for_expected_result(test, suffix))
|
| - for test in tests for suffix in BASELINE_SUFFIX_LIST
|
| - ])
|
| -
|
| + baseline_paths = set()
|
| + for test, build in test_baseline_set:
|
| + filenames = [self._file_name_for_expected_result(test, suffix) for suffix in BASELINE_SUFFIX_LIST]
|
| + port_baseline_dir = self._baseline_directory(build.builder_name)
|
| + baseline_paths.update({filesystem.join(port_baseline_dir, filename) for filename in filenames})
|
| + baseline_paths.update({filesystem.join(self._layout_tests_dir(), filename) for filename in filenames})
|
| return sorted(baseline_paths)
|
|
|
| def _layout_tests_dir(self):
|
| @@ -646,7 +657,7 @@ class RebaselineExpectations(AbstractParallelRebaselineCommand):
|
| super(RebaselineExpectations, self).__init__(options=[
|
| self.no_optimize_option,
|
| ] + self.platform_options)
|
| - self._test_prefix_list = None
|
| + self._test_baseline_set = None
|
|
|
| @staticmethod
|
| def _tests_to_rebaseline(port):
|
| @@ -668,27 +679,25 @@ class RebaselineExpectations(AbstractParallelRebaselineCommand):
|
|
|
| for test_name in tests:
|
| _log.info(' %s', test_name)
|
| - if test_name not in self._test_prefix_list:
|
| - self._test_prefix_list[test_name] = []
|
| - self._test_prefix_list[test_name].append(Build(builder_name))
|
| + self._test_baseline_set.add(test_name, Build(builder_name))
|
|
|
| def execute(self, options, args, tool):
|
| self._tool = tool
|
| + self._test_baseline_set = TestBaselineSet(tool)
|
| options.results_directory = None
|
| - self._test_prefix_list = {}
|
| port_names = tool.port_factory.all_port_names(options.platform)
|
| for port_name in port_names:
|
| self._add_tests_to_rebaseline(port_name)
|
| - if not self._test_prefix_list:
|
| + if not self._test_baseline_set:
|
| _log.warning('Did not find any tests marked Rebaseline.')
|
| return
|
|
|
| - self.rebaseline(options, self._test_prefix_list)
|
| + self.rebaseline(options, self._test_baseline_set)
|
|
|
|
|
| class Rebaseline(AbstractParallelRebaselineCommand):
|
| name = 'rebaseline'
|
| - help_text = 'Rebaseline tests with results from the build bots.'
|
| + help_text = 'Rebaseline tests with results from the continuous builders.'
|
| show_in_main_help = True
|
| argument_names = '[TEST_NAMES]'
|
|
|
| @@ -719,15 +728,12 @@ class Rebaseline(AbstractParallelRebaselineCommand):
|
| else:
|
| builders_to_check = self._builders_to_pull_from()
|
|
|
| - test_prefix_list = {}
|
| + test_baseline_set = TestBaselineSet(tool)
|
|
|
| for builder in builders_to_check:
|
| - for test in args:
|
| - if test not in test_prefix_list:
|
| - test_prefix_list[test] = []
|
| - test_prefix_list[test].append(Build(builder))
|
| + for test_prefix in args:
|
| + test_baseline_set.add(test_prefix, Build(builder))
|
|
|
| - if options.verbose:
|
| - _log.debug('rebaseline-json: ' + str(test_prefix_list))
|
| + _log.debug('Rebaselining: %s', test_baseline_set)
|
|
|
| - self.rebaseline(options, test_prefix_list)
|
| + self.rebaseline(options, test_baseline_set)
|
|
|