blob: 9141a8ee80d6094d6dd69e67b34815d4b7451037 [file] [log] [blame]
# 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.
"""A command to fetch new baselines from try jobs for the current CL."""
import json
import logging
import optparse
from webkitpy.common.net.git_cl import GitCL
from webkitpy.layout_tests.models.test_expectations import BASELINE_SUFFIX_LIST
from webkitpy.tool.commands.rebaseline import AbstractParallelRebaselineCommand
from webkitpy.w3c.wpt_manifest import WPTManifest
_log = logging.getLogger(__name__)
class RebaselineCL(AbstractParallelRebaselineCommand):
name = 'rebaseline-cl'
help_text = 'Fetches new baselines for a CL from test runs on try bots.'
long_help = ('By default, this command will check the latest try job results '
'for all platforms, and start try jobs for platforms with no '
'try jobs. Then, new baselines are downloaded for any tests '
'that are being rebaselined. After downloading, the baselines '
'for different platforms will be optimized (consolidated).')
show_in_main_help = True
def __init__(self):
super(RebaselineCL, self).__init__(options=[
optparse.make_option(
'--dry-run', action='store_true', default=False,
help='Dry run mode; list actions that would be performed but do not do anything.'),
optparse.make_option(
'--only-changed-tests', action='store_true', default=False,
help='Only download new baselines for tests that are changed in the CL.'),
optparse.make_option(
'--no-trigger-jobs', dest='trigger_jobs', action='store_false', default=True,
help='Do not trigger any try jobs.'),
self.no_optimize_option,
self.results_directory_option,
])
def execute(self, options, args, tool):
self._tool = tool
# TODO(qyearsley): Move this call to somewhere else.
WPTManifest.ensure_manifest(tool)
unstaged_baselines = self.unstaged_baselines()
if unstaged_baselines:
_log.error('Aborting: there are unstaged baselines:')
for path in unstaged_baselines:
_log.error(' %s', path)
return 1
issue_number = self._get_issue_number()
if issue_number is None:
_log.error('No issue number for current branch.')
return 1
_log.debug('Issue number for current branch: %s', issue_number)
builds = self.git_cl().latest_try_jobs(self._try_bots())
builders_with_pending_builds = self.builders_with_pending_builds(builds)
if builders_with_pending_builds:
_log.info('There are existing pending builds for:')
for builder in sorted(builders_with_pending_builds):
_log.info(' %s', builder)
builders_with_no_results = self.builders_with_no_results(builds)
if options.trigger_jobs and builders_with_no_results:
self.trigger_builds(builders_with_no_results)
_log.info('Please re-run webkit-patch rebaseline-cl once all pending try jobs have finished.')
return 1
if builders_with_no_results:
# TODO(qyearsley): Support trying to continue as long as there are
# some results from some builder; see http://crbug.com/673966.
_log.error('The following builders have no results:')
for builder in builders_with_no_results:
_log.error(' %s', builder)
return 1
_log.debug('Getting results for issue %d.', issue_number)
builds_to_results = self._fetch_results(builds)
if builds_to_results is None:
return 1
test_prefix_list = {}
if args:
for test in args:
test_prefix_list[test] = {b: BASELINE_SUFFIX_LIST for b in builds}
else:
test_prefix_list = self._test_prefix_list(
builds_to_results,
only_changed_tests=options.only_changed_tests)
self._log_test_prefix_list(test_prefix_list)
if not options.dry_run:
self.rebaseline(options, test_prefix_list)
return 0
def _get_issue_number(self):
"""Returns the current CL issue number, or None."""
issue = self.git_cl().get_issue_number()
if not issue.isdigit():
return None
return int(issue)
def git_cl(self):
"""Returns a GitCL instance. Can be overridden for tests."""
return GitCL(self._tool)
def trigger_builds(self, builders):
_log.info('Triggering try jobs for:')
for builder in sorted(builders):
_log.info(' %s', builder)
self.git_cl().trigger_try_jobs(builders)
def builders_with_no_results(self, builds):
"""Returns the set of builders that don't have finished results."""
builders_with_no_builds = set(self._try_bots()) - {b.builder_name for b in builds}
return builders_with_no_builds | self.builders_with_pending_builds(builds)
def builders_with_pending_builds(self, builds):
"""Returns the set of builders that have pending builds."""
return {b.builder_name for b in builds if b.build_number is None}
def _try_bots(self):
"""Returns a collection of try bot builders to fetch results for."""
return self._tool.builders.all_try_builder_names()
def _fetch_results(self, builds):
"""Fetches results for all of the given builds.
There should be a one-to-one correspondence between Builds, supported
platforms, and try bots. If not all of the builds can be fetched, then
continuing with rebaselining may yield incorrect results, when the new
baselines are deduped, an old baseline may be kept for the platform
that's missing results.
Args:
builds: A list of Build objects.
Returns:
A dict mapping Build to LayoutTestResults, or None if any results
were not available.
"""
buildbot = self._tool.buildbot
results = {}
for build in builds:
results_url = buildbot.results_url(build.builder_name, build.build_number)
layout_test_results = buildbot.fetch_results(build)
if layout_test_results is None:
_log.error(
'Failed to fetch results from "%s".\n'
'Try starting a new job for %s by running :\n'
' git cl try -b %s',
results_url, build.builder_name, build.builder_name)
return None
results[build] = layout_test_results
return results
def _test_prefix_list(self, builds_to_results, only_changed_tests):
"""Returns a dict which lists the set of baselines to fetch.
The dict that is returned is a dict of tests to Build objects
to baseline file extensions.
Args:
builds_to_results: A dict mapping Builds to LayoutTestResults.
only_changed_tests: Whether to only include baselines for tests that
are changed in this CL. If False, all new baselines for failing
tests will be downloaded, even for tests that were not modified.
Returns:
A dict containing information about which new baselines to download.
"""
builds_to_tests = {}
for build, results in builds_to_results.iteritems():
builds_to_tests[build] = self._tests_to_rebaseline(build, results)
if only_changed_tests:
files_in_cl = self._tool.git().changed_files(diff_filter='AM')
# In the changed files list from Git, paths always use "/" as
# the path separator, and they're always relative to repo root.
# TODO(qyearsley): Do this without using a hard-coded constant.
test_base = 'third_party/WebKit/LayoutTests/'
tests_in_cl = [f[len(test_base):] for f in files_in_cl if f.startswith(test_base)]
result = {}
for build, tests in builds_to_tests.iteritems():
for test in tests:
if only_changed_tests and test not in tests_in_cl:
continue
if test not in result:
result[test] = {}
result[test][build] = BASELINE_SUFFIX_LIST
return result
def _tests_to_rebaseline(self, build, layout_test_results):
"""Fetches a list of tests that should be rebaselined for some build."""
unexpected_results = layout_test_results.didnt_run_as_expected_results()
tests = sorted(r.test_name() for r in unexpected_results
if r.is_missing_baseline() or r.has_mismatch_result())
new_failures = self._fetch_tests_with_new_failures(build)
if new_failures is None:
_log.warning('No retry summary available for build %s.', build)
else:
tests = [t for t in tests if t in new_failures]
return tests
def _fetch_tests_with_new_failures(self, build):
"""For a given try job, lists tests that only occurred with the patch."""
buildbot = self._tool.buildbot
content = buildbot.fetch_retry_summary_json(build)
if content is None:
return None
try:
retry_summary = json.loads(content)
return retry_summary['failures']
except (ValueError, KeyError):
_log.warning('Unexpected retry summary content:\n%s', content)
return None
@staticmethod
def _log_test_prefix_list(test_prefix_list):
"""Logs the tests to download new baselines for."""
if not test_prefix_list:
_log.info('No tests to rebaseline; exiting.')
return
_log.debug('Tests to rebaseline:')
for test, builds in test_prefix_list.iteritems():
_log.debug(' %s:', test)
for build in sorted(builds):
_log.debug(' %s', build)