blob: 7b1bea932fe69cdab54c575a57e83d5a25399e76 [file] [log] [blame]
# Copyright (c) 2010 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR/ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import json
import logging
import optparse
import re
import sys
import traceback
from webkitpy.common.memoized import memoized
from webkitpy.common.net.buildbot import Build
from webkitpy.common.system.executive import ScriptError
from webkitpy.layout_tests.models.testharness_results import is_all_pass_testharness_result
from webkitpy.layout_tests.models.test_expectations import TestExpectations, BASELINE_SUFFIX_LIST, SKIP
from webkitpy.layout_tests.port import factory
from webkitpy.tool.commands.command import Command
_log = logging.getLogger(__name__)
class AbstractRebaseliningCommand(Command):
"""Base class for rebaseline-related commands."""
# Not overriding execute() - pylint: disable=abstract-method
no_optimize_option = optparse.make_option(
'--no-optimize', dest='optimize', action='store_false', default=True,
help=('Do not optimize (de-duplicate) the expectations after rebaselining '
'(default is to de-dupe automatically). You can use "webkit-patch '
'optimize-baselines" to optimize separately.'))
platform_options = factory.platform_options(use_globs=True)
results_directory_option = optparse.make_option(
'--results-directory', help='Local results directory to use.')
suffixes_option = optparse.make_option(
'--suffixes', default=','.join(BASELINE_SUFFIX_LIST), action='store',
help='Comma-separated-list of file types to rebaseline.')
builder_option = optparse.make_option(
'--builder', help='Builder to pull new baselines from.')
test_option = optparse.make_option('--test', help='Test to rebaseline.')
build_number_option = optparse.make_option(
'--build-number', default=None, type='int',
help='Optional build number; if not given, the latest build is used.')
def __init__(self, options=None):
super(AbstractRebaseliningCommand, self).__init__(options=options)
self._baseline_suffix_list = BASELINE_SUFFIX_LIST
self.expectation_line_changes = ChangeSet()
self._tool = None
def _print_expectation_line_changes(self):
print(json.dumps(self.expectation_line_changes.to_dict()))
def _baseline_directory(self, builder_name):
port = self._tool.port_factory.get_from_builder_name(builder_name)
return port.baseline_version_dir()
def _test_root(self, test_name):
return self._tool.filesystem.splitext(test_name)[0]
def _file_name_for_actual_result(self, test_name, suffix):
return '%s-actual.%s' % (self._test_root(test_name), suffix)
def _file_name_for_expected_result(self, test_name, suffix):
return '%s-expected.%s' % (self._test_root(test_name), suffix)
class ChangeSet(object):
"""A record of TestExpectation lines to remove.
TODO(qyearsley): Remove this class, track list of lines to remove directly
in an attribute of AbstractRebaseliningCommand.
"""
def __init__(self, lines_to_remove=None):
self.lines_to_remove = lines_to_remove or {}
def remove_line(self, test, builder):
if test not in self.lines_to_remove:
self.lines_to_remove[test] = []
self.lines_to_remove[test].append(builder)
def to_dict(self):
remove_lines = []
for test in self.lines_to_remove:
for builder in self.lines_to_remove[test]:
remove_lines.append({'test': test, 'builder': builder})
return {'remove-lines': remove_lines}
@staticmethod
def from_dict(change_dict):
lines_to_remove = {}
if 'remove-lines' in change_dict:
for line_to_remove in change_dict['remove-lines']:
test = line_to_remove['test']
builder = line_to_remove['builder']
if test not in lines_to_remove:
lines_to_remove[test] = []
lines_to_remove[test].append(builder)
return ChangeSet(lines_to_remove=lines_to_remove)
def update(self, other):
assert isinstance(other, ChangeSet)
assert isinstance(other.lines_to_remove, dict)
for test in other.lines_to_remove:
if test not in self.lines_to_remove:
self.lines_to_remove[test] = []
self.lines_to_remove[test].extend(other.lines_to_remove[test])
class CopyExistingBaselinesInternal(AbstractRebaseliningCommand):
name = 'copy-existing-baselines-internal'
help_text = ('Copy existing baselines down one level in the baseline order to ensure '
"new baselines don't break existing passing platforms.")
def __init__(self):
super(CopyExistingBaselinesInternal, self).__init__(options=[
self.results_directory_option,
self.suffixes_option,
self.builder_option,
self.test_option,
])
@memoized
def _immediate_predecessors_in_fallback(self, path_to_rebaseline):
"""Returns the predecessor directories in the baseline fall-back graph.
The platform-specific fall-back baseline directories form a tree; the
"immediate predecessors" are the children nodes For example, if the
baseline fall-back graph includes:
"mac10.9" -> "mac10.10/"
"mac10.10/" -> "mac/"
"retina/" -> "mac/"
Then, the "immediate predecessors" are:
"mac/": ["mac10.10/", "retina/"]
"mac10.10/": ["mac10.9/"]
"mac10.9/", "retina/": []
"""
port_names = self._tool.port_factory.all_port_names()
immediate_predecessors = []
for port_name in port_names:
port = self._tool.port_factory.get(port_name)
baseline_search_path = port.baseline_search_path()
try:
index = baseline_search_path.index(path_to_rebaseline)
if index:
immediate_predecessors.append(self._tool.filesystem.basename(baseline_search_path[index - 1]))
except ValueError:
# baseline_search_path.index() throws a ValueError if the item isn't in the list.
pass
return immediate_predecessors
def _port_for_primary_baseline(self, baseline):
"""Returns a Port object for the given baseline directory base name."""
for port in [self._tool.port_factory.get(port_name) for port_name in self._tool.port_factory.all_port_names()]:
if self._tool.filesystem.basename(port.baseline_version_dir()) == baseline:
return port
raise Exception('Failed to find port for primary baseline %s.' % baseline)
def _copy_existing_baseline(self, builder_name, test_name, suffix):
"""Copies the baseline for the given builder to all "predecessor" directories."""
baseline_directory = self._baseline_directory(builder_name)
ports = [self._port_for_primary_baseline(baseline)
for baseline in self._immediate_predecessors_in_fallback(baseline_directory)]
old_baselines = []
new_baselines = []
# Need to gather all the baseline paths before modifying the filesystem since
# the modifications can affect the results of port.expected_filename.
for port in ports:
old_baseline = port.expected_filename(test_name, '.' + suffix)
if not self._tool.filesystem.exists(old_baseline):
_log.debug('No existing baseline for %s.', test_name)
continue
new_baseline = self._tool.filesystem.join(
port.baseline_version_dir(),
self._file_name_for_expected_result(test_name, suffix))
if self._tool.filesystem.exists(new_baseline):
_log.debug('Existing baseline at %s, not copying over it.', new_baseline)
continue
generic_expectations = TestExpectations(port, tests=[test_name], include_overrides=False)
full_expectations = TestExpectations(port, tests=[test_name], include_overrides=True)
# TODO(qyearsley): Change Port.skips_test so that this can be simplified.
if SKIP in full_expectations.get_expectations(test_name):
_log.debug('%s is skipped (perhaps temporarily) on %s.', test_name, port.name())
continue
if port.skips_test(test_name, generic_expectations, full_expectations):
_log.debug('%s is skipped on %s.', test_name, port.name())
continue
old_baselines.append(old_baseline)
new_baselines.append(new_baseline)
for i in range(len(old_baselines)):
old_baseline = old_baselines[i]
new_baseline = new_baselines[i]
_log.debug('Copying baseline from %s to %s.', old_baseline, new_baseline)
self._tool.filesystem.maybe_make_directory(self._tool.filesystem.dirname(new_baseline))
self._tool.filesystem.copyfile(old_baseline, new_baseline)
def execute(self, options, args, tool):
self._tool = tool
for suffix in options.suffixes.split(','):
self._copy_existing_baseline(options.builder, options.test, suffix)
class RebaselineTest(AbstractRebaseliningCommand):
name = 'rebaseline-test-internal'
help_text = 'Rebaseline a single test from a buildbot. Only intended for use by other webkit-patch commands.'
def __init__(self):
super(RebaselineTest, self).__init__(options=[
self.results_directory_option,
self.suffixes_option,
self.builder_option,
self.test_option,
self.build_number_option,
])
def _save_baseline(self, data, target_baseline):
if not data:
_log.debug('No baseline data to save.')
return
filesystem = self._tool.filesystem
filesystem.maybe_make_directory(filesystem.dirname(target_baseline))
filesystem.write_binary_file(target_baseline, data)
def _rebaseline_test(self, builder_name, test_name, suffix, results_url):
baseline_directory = self._baseline_directory(builder_name)
source_baseline = '%s/%s' % (results_url, self._file_name_for_actual_result(test_name, suffix))
target_baseline = self._tool.filesystem.join(baseline_directory, self._file_name_for_expected_result(test_name, suffix))
_log.debug('Retrieving source %s for target %s.', source_baseline, target_baseline)
self._save_baseline(self._tool.web.get_binary(source_baseline, return_none_on_404=True),
target_baseline)
def _rebaseline_test_and_update_expectations(self, options):
self._baseline_suffix_list = options.suffixes.split(',')
port = self._tool.port_factory.get_from_builder_name(options.builder)
if port.reference_files(options.test):
if 'png' in self._baseline_suffix_list:
_log.warning('Cannot rebaseline image result for reftest: %s', options.test)
return
assert self._baseline_suffix_list == ['txt']
if options.results_directory:
results_url = 'file://' + options.results_directory
else:
results_url = self._tool.buildbot.results_url(options.builder, build_number=options.build_number)
for suffix in self._baseline_suffix_list:
self._rebaseline_test(options.builder, options.test, suffix, results_url)
self.expectation_line_changes.remove_line(test=options.test, builder=options.builder)
def execute(self, options, args, tool):
self._tool = tool
self._rebaseline_test_and_update_expectations(options)
self._print_expectation_line_changes()
class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
"""Base class for rebaseline commands that do some tasks in parallel."""
# Not overriding execute() - pylint: disable=abstract-method
def __init__(self, options=None):
super(AbstractParallelRebaselineCommand, self).__init__(options=options)
def _release_builders(self):
"""Returns a list of builder names for continuous release builders.
The release builders cycle much faster than the debug ones and cover all the platforms.
"""
release_builders = []
for builder_name in self._tool.builders.all_continuous_builder_names():
port = self._tool.port_factory.get_from_builder_name(builder_name)
if port.test_configuration().build_type == 'release':
release_builders.append(builder_name)
return release_builders
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)
for line in stderr.splitlines():
_log.warning(line)
except ScriptError:
traceback.print_exc(file=sys.stderr)
def _builders_to_fetch_from(self, builders_to_check):
"""Returns the subset of builders that will cover all of the baseline
search paths used in the input list.
In particular, if the input list contains both Release and Debug
versions of a configuration, we *only* return the Release version
(since we don't save debug versions of baselines).
Args:
builders_to_check: List of builder names.
"""
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)
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]
def _rebaseline_commands(self, test_prefix_list, 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
else:
actual_failures_suffixes = self._suffixes_for_actual_failures(
test, build, test_prefix_list[test_prefix][build])
if not actual_failures_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
suffixes = ','.join(actual_failures_suffixes)
args = ['--suffixes', 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]))
return copy_baseline_commands, rebaseline_commands, lines_to_remove
@staticmethod
def _extract_expectation_line_changes(command_results):
"""Parses the JSON lines from sub-command output and returns the result as a ChangeSet."""
change_set = ChangeSet()
for _, stdout, _ in command_results:
updated = False
for line in filter(None, stdout.splitlines()):
try:
parsed_line = json.loads(line)
change_set.update(ChangeSet.from_dict(parsed_line))
updated = True
except ValueError:
_log.debug('"%s" is not a JSON object, ignoring', line)
if not updated:
# TODO(qyearsley): This probably should be an error. See http://crbug.com/649412.
_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, test_prefix_list[test][build]))
# No need to optimize baselines for a test with no failures.
if not all_suffixes:
continue
# FIXME: We should propagate the platform options as well.
cmd_line = ['--suffixes', ','.join(all_suffixes), test]
if verbose:
cmd_line.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]))
return optimize_commands
def _update_expectations_files(self, lines_to_remove):
# FIXME: This routine is way too expensive. We're creating O(n ports) TestExpectations objects.
# This is slow and uses a lot of memory.
tests = lines_to_remove.keys()
to_remove = []
# This is so we remove lines for builders that skip this test, e.g. Android skips most
# tests and we don't want to leave stray [ Android ] lines in TestExpectations..
# This is only necessary for "webkit-patch rebaseline" and for rebaselining expected
# failures from garden-o-matic. rebaseline-expectations and auto-rebaseline will always
# pass the exact set of ports to rebaseline.
for port_name in self._tool.port_factory.all_port_names():
port = self._tool.port_factory.get(port_name)
generic_expectations = TestExpectations(port, tests=tests, include_overrides=False)
full_expectations = TestExpectations(port, tests=tests, include_overrides=True)
for test in tests:
if port.skips_test(test, generic_expectations, full_expectations):
for test_configuration in port.all_test_configurations():
if test_configuration.version == port.test_configuration().version:
to_remove.append((test, test_configuration))
for test in lines_to_remove:
for builder in lines_to_remove[test]:
port = self._tool.port_factory.get_from_builder_name(builder)
for test_configuration in port.all_test_configurations():
if test_configuration.version == port.test_configuration().version:
to_remove.append((test, test_configuration))
port = self._tool.port_factory.get()
expectations = TestExpectations(port, include_overrides=False)
expectations_string = expectations.remove_configurations(to_remove)
path = port.path_to_generic_test_expectations_file()
self._tool.filesystem.write_text_file(path, expectations_string)
def _run_in_parallel(self, commands):
if not commands:
return {}
command_results = self._tool.executive.run_in_parallel(commands)
for _, _, stderr in command_results:
if stderr:
_log.error(stderr)
change_set = self._extract_expectation_line_changes(command_results)
return change_set.lines_to_remove
def rebaseline(self, options, test_prefix_list):
"""Downloads new baselines in parallel, then updates expectations files
and optimizes baselines.
Args:
options: An object with the options passed to the current command.
test_prefix_list: A map of test names to Build objects to file suffixes
for new baselines. For example:
{
"some/test.html": {Build("builder-1", 412): ["txt"], Build("builder-2", 100): ["txt"]},
"some/other.html": {Build("builder-1", 412): ["txt"]}
}
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.
"""
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()):
_log.info('Rebaselining %s', test)
for build, suffixes in sorted(builds_to_check.items()):
_log.debug(' %s: %s', build, ','.join(suffixes))
copy_baseline_commands, rebaseline_commands, extra_lines_to_remove = self._rebaseline_commands(
test_prefix_list, options)
lines_to_remove = {}
self._run_in_parallel(copy_baseline_commands)
lines_to_remove = self._run_in_parallel(rebaseline_commands)
for test in extra_lines_to_remove:
if test in lines_to_remove:
lines_to_remove[test] = lines_to_remove[test] + extra_lines_to_remove[test]
else:
lines_to_remove[test] = extra_lines_to_remove[test]
if lines_to_remove:
self._update_expectations_files(lines_to_remove)
if options.optimize:
# 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._remove_all_pass_testharness_baselines(test_prefix_list)
self._tool.git().add_list(self.unstaged_baselines())
def unstaged_baselines(self):
"""Returns absolute paths for unstaged (including untracked) baselines."""
baseline_re = re.compile(r'.*[\\/]LayoutTests[\\/].*-expected\.(txt|png|wav)$')
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):
"""Removes all of the all-PASS baselines for the given builders and tests.
In general, for testharness.js tests, the absence of a baseline
indicates that the test is expected to pass. When rebaselining,
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)
for path in baseline_paths:
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):
"""Return 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 to baseline suffixes.
TODO(qyearsley): If a class is added to replace test_prefix_list,
then this can be made a method on that class.
Returns:
A list of absolute paths to possible baseline files,
which may or may not exist on the local filesystem.
"""
filesystem = self._tool.filesystem
baseline_paths = []
port = self._tool.port_factory.get()
for test_prefix in test_prefix_list:
tests = port.tests([test_prefix])
all_suffixes = set()
for build, suffixes in test_prefix_list[test_prefix].iteritems():
all_suffixes.update(suffixes)
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 suffixes
])
baseline_paths.extend([
filesystem.join(self._layout_tests_dir(), self._file_name_for_expected_result(test, suffix))
for test in tests for suffix in all_suffixes
])
return sorted(baseline_paths)
def _layout_tests_dir(self):
return self._tool.port_factory.get().layout_tests_dir()
def _suffixes_for_actual_failures(self, test, build, existing_suffixes):
"""Gets the baseline suffixes for actual mismatch failures in some results.
Args:
test: A full test path string.
build: A Build object.
existing_suffixes: A collection of all suffixes to consider.
Returns:
A set of file suffix strings.
"""
results = self._tool.buildbot.fetch_results(build)
if not results:
_log.debug('No results found for build %s', build)
return set()
test_result = results.result_for_test(test)
if not test_result:
_log.debug('No test result for test %s in build %s', test, build)
return set()
return set(existing_suffixes) & TestExpectations.suffixes_for_test_result(test_result)
class RebaselineJson(AbstractParallelRebaselineCommand):
name = 'rebaseline-json'
help_text = 'Rebaseline based off JSON passed to stdin. Intended to only be called from other scripts.'
def __init__(self,):
super(RebaselineJson, self).__init__(options=[
self.no_optimize_option,
self.results_directory_option,
])
def execute(self, options, args, tool):
self._tool = tool
self.rebaseline(options, json.loads(sys.stdin.read()))
class RebaselineExpectations(AbstractParallelRebaselineCommand):
name = 'rebaseline-expectations'
help_text = 'Rebaselines the tests indicated in TestExpectations.'
show_in_main_help = True
def __init__(self):
super(RebaselineExpectations, self).__init__(options=[
self.no_optimize_option,
] + self.platform_options)
self._test_prefix_list = None
@staticmethod
def _tests_to_rebaseline(port):
tests_to_rebaseline = {}
for path, value in port.expectations_dict().items():
expectations = TestExpectations(port, include_overrides=False, expectations_dict={path: value})
for test in expectations.get_rebaselining_failures():
suffixes = TestExpectations.suffixes_for_expectations(expectations.get_expectations(test))
tests_to_rebaseline[test] = suffixes or BASELINE_SUFFIX_LIST
return tests_to_rebaseline
def _add_tests_to_rebaseline(self, port_name):
builder_name = self._tool.builders.builder_name_for_port_name(port_name)
if not builder_name:
return
tests = self._tests_to_rebaseline(self._tool.port_factory.get(port_name)).items()
if tests:
_log.info('Retrieving results for %s from %s.', port_name, builder_name)
for test_name, suffixes in tests:
_log.info(' %s (%s)', test_name, ','.join(suffixes))
if test_name not in self._test_prefix_list:
self._test_prefix_list[test_name] = {}
self._test_prefix_list[test_name][Build(builder_name)] = suffixes
def execute(self, options, args, tool):
self._tool = 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:
_log.warning('Did not find any tests marked Rebaseline.')
return
self.rebaseline(options, self._test_prefix_list)
class Rebaseline(AbstractParallelRebaselineCommand):
name = 'rebaseline'
help_text = 'Rebaseline tests with results from the build bots.'
show_in_main_help = True
argument_names = '[TEST_NAMES]'
def __init__(self):
super(Rebaseline, self).__init__(options=[
self.no_optimize_option,
# FIXME: should we support the platform options in addition to (or instead of) --builders?
self.suffixes_option,
self.results_directory_option,
optparse.make_option('--builders', default=None, action='append',
help=('Comma-separated-list of builders to pull new baselines from '
'(can also be provided multiple times).')),
])
def _builders_to_pull_from(self):
return self._tool.user.prompt_with_list(
'Which builder to pull results from:', self._release_builders(), can_choose_multiple=True)
def execute(self, options, args, tool):
self._tool = tool
if not args:
_log.error('Must list tests to rebaseline.')
return
if options.builders:
builders_to_check = []
for builder_names in options.builders:
builders_to_check += builder_names.split(',')
else:
builders_to_check = self._builders_to_pull_from()
test_prefix_list = {}
suffixes_to_update = options.suffixes.split(',')
for builder in builders_to_check:
for test in args:
if test not in test_prefix_list:
test_prefix_list[test] = {}
build = Build(builder)
test_prefix_list[test][build] = suffixes_to_update
if options.verbose:
_log.debug('rebaseline-json: ' + str(test_prefix_list))
self.rebaseline(options, test_prefix_list)