# Copyright (c) 2009 Google Inc. All rights reserved.
# Copyright (c) 2009 Apple 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 optparse import make_option
from webkitpy.common.checkout.commitinfo import CommitInfo
from webkitpy.common.config.committers import CommitterList
from webkitpy.common.net.buildbot import BuildBot
from webkitpy.common.system.user import User
from webkitpy.tool.grammar import pluralize
from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
from webkitpy.common.system.deprecated_logging import log
class BugsToCommit(AbstractDeclarativeCommand):
name = "bugs-to-commit"
help_text = "List bugs in the commit-queue"
def execute(self, options, args, tool):
# FIXME: This command is poorly named. It's fetching the commit-queue list here. The name implies it's fetching pending-commit (all r+'d patches).
bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
for bug_id in bug_ids:
print "%s" % bug_id
class PatchesInCommitQueue(AbstractDeclarativeCommand):
name = "patches-in-commit-queue"
help_text = "List patches in the commit-queue"
def execute(self, options, args, tool):
patches = tool.bugs.queries.fetch_patches_from_commit_queue()
log("Patches in commit queue:")
for patch in patches:
print patch.url()
class PatchesToCommitQueue(AbstractDeclarativeCommand):
name = "patches-to-commit-queue"
help_text = "Patches which should be added to the commit queue"
def __init__(self):
options = [
make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
]
AbstractDeclarativeCommand.__init__(self, options=options)
@staticmethod
def _needs_commit_queue(patch):
if patch.commit_queue() == "+": # If it's already cq+, ignore the patch.
log("%s already has cq=%s" % (patch.id(), patch.commit_queue()))
return False
# We only need to worry about patches from contributers who are not yet committers.
committer_record = CommitterList().committer_by_email(patch.attacher_email())
if committer_record:
log("%s committer = %s" % (patch.id(), committer_record))
return not committer_record
def execute(self, options, args, tool):
patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
patches_needing_cq = filter(self._needs_commit_queue, patches)
if options.bugs:
bugs_needing_cq = map(lambda patch: patch.bug_id(), patches_needing_cq)
bugs_needing_cq = sorted(set(bugs_needing_cq))
for bug_id in bugs_needing_cq:
print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
else:
for patch in patches_needing_cq:
print "%s" % tool.bugs.attachment_url_for_id(patch.id(), action="edit")
class PatchesToReview(AbstractDeclarativeCommand):
name = "patches-to-review"
help_text = "List patches that are pending review"
def execute(self, options, args, tool):
patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue()
log("Patches pending review:")
for patch_id in patch_ids:
print patch_id
class LastGreenRevision(AbstractDeclarativeCommand):
name = "last-green-revision"
help_text = "Prints the last known good revision"
def execute(self, options, args, tool):
print self.tool.buildbot.last_green_revision()
class WhatBroke(AbstractDeclarativeCommand):
name = "what-broke"
help_text = "Print failing buildbots (%s) and what revisions broke them" % BuildBot.default_host
def _print_builder_line(self, builder_name, max_name_width, status_message):
print "%s : %s" % (builder_name.ljust(max_name_width), status_message)
# FIXME: This is slightly different from Builder.suspect_revisions_for_green_to_red_transition
# due to needing to detect the "hit the limit" case an print a special message.
def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True):
builder = self.tool.buildbot.builder_with_name(builder_status["name"])
red_build = builder.build(builder_status["build_number"])
(last_green_build, first_red_build) = builder.find_failure_transition(red_build)
if not first_red_build:
self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)")
return
if not last_green_build:
self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % first_red_build.revision())
return
suspect_revisions = range(first_red_build.revision(), last_green_build.revision(), -1)
suspect_revisions.reverse()
first_failure_message = ""
if (first_red_build == builder.build(builder_status["build_number"])):
first_failure_message = " FIRST FAILURE, possibly a flaky test"
self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (suspect_revisions, first_failure_message))
for revision in suspect_revisions:
commit_info = self.tool.checkout().commit_info_for_revision(revision)
if commit_info:
print commit_info.blame_string(self.tool.bugs)
else:
print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
def execute(self, options, args, tool):
builder_statuses = tool.buildbot.builder_statuses()
longest_builder_name = max(map(len, map(lambda builder: builder["name"], builder_statuses)))
failing_builders = 0
for builder_status in builder_statuses:
# If the builder is green, print OK, exit.
if builder_status["is_green"]:
continue
self._print_blame_information_for_builder(builder_status, name_width=longest_builder_name)
failing_builders += 1
if failing_builders:
print "%s of %s are failing" % (failing_builders, pluralize("builder", len(builder_statuses)))
else:
print "All builders are passing!"
class WhoBrokeIt(AbstractDeclarativeCommand):
name = "who-broke-it"
help_text = "Print a list of revisions causing failures on %s" % BuildBot.default_host
def execute(self, options, args, tool):
for revision, builders in self.tool.buildbot.revisions_causing_failures(False).items():
print "r%s appears to have broken %s" % (revision, [builder.name() for builder in builders])
class ResultsFor(AbstractDeclarativeCommand):
name = "results-for"
help_text = "Print a list of failures for the passed revision from bots on %s" % BuildBot.default_host
argument_names = "REVISION"
def _print_layout_test_results(self, results):
if not results:
print " No results."
return
for title, files in results.parsed_results().items():
print " %s" % title
for filename in files:
print " %s" % filename
def execute(self, options, args, tool):
builders = self.tool.buildbot.builders()
for builder in builders:
print "%s:" % builder.name()
build = builder.build_for_revision(args[0], allow_failed_lookups=True)
self._print_layout_test_results(build.layout_test_results())
class FailureReason(AbstractDeclarativeCommand):
name = "failure-reason"
help_text = "Lists revisions where individual test failures started at %s" % BuildBot.default_host
def _print_blame_information_for_transition(self, green_build, red_build, failing_tests):
suspect_revisions = green_build.builder().suspect_revisions_for_transition(green_build, red_build)
print "SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests)
print "Suspect revisions:"
for revision in suspect_revisions:
commit_info = self.tool.checkout().commit_info_for_revision(revision)
if commit_info:
print commit_info.blame_string(self.tool.bugs)
else:
print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
def _explain_failures_for_builder(self, builder, start_revision):
print "Examining failures for \"%s\", starting at r%s" % (builder.name(), start_revision)
revision_to_test = start_revision
build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
layout_test_results = build.layout_test_results()
if not layout_test_results:
# FIXME: This could be made more user friendly.
print "Failed to load layout test results; can't continue. (start revision = r%s)" % start_revision
return 1
results_to_explain = set(layout_test_results.failing_tests())
last_build_with_results = build
print "Starting at %s" % revision_to_test
while results_to_explain:
revision_to_test -= 1
new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
if not new_build:
print "No build for %s" % revision_to_test
continue
build = new_build
latest_results = build.layout_test_results()
if not latest_results:
print "No results build %s (r%s)" % (build._number, build.revision())
continue
failures = set(latest_results.failing_tests())
if len(failures) >= 20:
# FIXME: We may need to move this logic into the LayoutTestResults class.
# The buildbot stops runs after 20 failures so we don't have full results to work with here.
print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision())
continue
fixed_results = results_to_explain - failures
if not fixed_results:
print "No change in build %s (r%s), %s unexplained failures (%s in this build)" % (build._number, build.revision(), len(results_to_explain), len(failures))
last_build_with_results = build
continue
self._print_blame_information_for_transition(build, last_build_with_results, fixed_results)
last_build_with_results = build
results_to_explain -= fixed_results
if results_to_explain:
print "Failed to explain failures: %s" % results_to_explain
return 1
print "Explained all results for %s" % builder.name()
return 0
def _builder_to_explain(self):
builder_statuses = self.tool.buildbot.builder_statuses()
red_statuses = [status for status in builder_statuses if not status["is_green"]]
print "%s failing" % (pluralize("builder", len(red_statuses)))
builder_choices = [status["name"] for status in red_statuses]
# We could offer an "All" choice here.
chosen_name = User.prompt_with_list("Which builder to diagnose:", builder_choices)
# FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object.
for status in red_statuses:
if status["name"] == chosen_name:
return (self.tool.buildbot.builder_with_name(chosen_name), status["built_revision"])
def execute(self, options, args, tool):
(builder, latest_revision) = self._builder_to_explain()
start_revision = self.tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision
if not start_revision:
print "Revision required."
return 1
return self._explain_failures_for_builder(builder, start_revision=int(start_revision))
class TreeStatus(AbstractDeclarativeCommand):
name = "tree-status"
help_text = "Print the status of the %s buildbots" % BuildBot.default_host
long_help = """Fetches build status from http://build.webkit.org/one_box_per_builder
and displayes the status of each builder."""
def execute(self, options, args, tool):
for builder in tool.buildbot.builder_statuses():
status_string = "ok" if builder["is_green"] else "FAIL"
print "%s : %s" % (status_string.ljust(4), builder["name"])