WebKitTools/Scripts/webkitpy/tool/commands/queries.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     1 # Copyright (c) 2009 Google Inc. All rights reserved.
       
     2 # Copyright (c) 2009 Apple Inc. All rights reserved.
       
     3 #
       
     4 # Redistribution and use in source and binary forms, with or without
       
     5 # modification, are permitted provided that the following conditions are
       
     6 # met:
       
     7 # 
       
     8 #     * Redistributions of source code must retain the above copyright
       
     9 # notice, this list of conditions and the following disclaimer.
       
    10 #     * Redistributions in binary form must reproduce the above
       
    11 # copyright notice, this list of conditions and the following disclaimer
       
    12 # in the documentation and/or other materials provided with the
       
    13 # distribution.
       
    14 #     * Neither the name of Google Inc. nor the names of its
       
    15 # contributors may be used to endorse or promote products derived from
       
    16 # this software without specific prior written permission.
       
    17 # 
       
    18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
       
    19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
       
    20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
       
    21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
       
    22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
       
    23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
       
    24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
       
    25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
       
    26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
       
    28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    29 
       
    30 
       
    31 from optparse import make_option
       
    32 
       
    33 from webkitpy.common.checkout.commitinfo import CommitInfo
       
    34 from webkitpy.common.config.committers import CommitterList
       
    35 from webkitpy.common.net.buildbot import BuildBot
       
    36 from webkitpy.common.system.user import User
       
    37 from webkitpy.tool.grammar import pluralize
       
    38 from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
       
    39 from webkitpy.common.system.deprecated_logging import log
       
    40 
       
    41 
       
    42 class BugsToCommit(AbstractDeclarativeCommand):
       
    43     name = "bugs-to-commit"
       
    44     help_text = "List bugs in the commit-queue"
       
    45 
       
    46     def execute(self, options, args, tool):
       
    47         # 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).
       
    48         bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
       
    49         for bug_id in bug_ids:
       
    50             print "%s" % bug_id
       
    51 
       
    52 
       
    53 class PatchesInCommitQueue(AbstractDeclarativeCommand):
       
    54     name = "patches-in-commit-queue"
       
    55     help_text = "List patches in the commit-queue"
       
    56 
       
    57     def execute(self, options, args, tool):
       
    58         patches = tool.bugs.queries.fetch_patches_from_commit_queue()
       
    59         log("Patches in commit queue:")
       
    60         for patch in patches:
       
    61             print patch.url()
       
    62 
       
    63 
       
    64 class PatchesToCommitQueue(AbstractDeclarativeCommand):
       
    65     name = "patches-to-commit-queue"
       
    66     help_text = "Patches which should be added to the commit queue"
       
    67     def __init__(self):
       
    68         options = [
       
    69             make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
       
    70         ]
       
    71         AbstractDeclarativeCommand.__init__(self, options=options)
       
    72 
       
    73     @staticmethod
       
    74     def _needs_commit_queue(patch):
       
    75         if patch.commit_queue() == "+": # If it's already cq+, ignore the patch.
       
    76             log("%s already has cq=%s" % (patch.id(), patch.commit_queue()))
       
    77             return False
       
    78 
       
    79         # We only need to worry about patches from contributers who are not yet committers.
       
    80         committer_record = CommitterList().committer_by_email(patch.attacher_email())
       
    81         if committer_record:
       
    82             log("%s committer = %s" % (patch.id(), committer_record))
       
    83         return not committer_record
       
    84 
       
    85     def execute(self, options, args, tool):
       
    86         patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
       
    87         patches_needing_cq = filter(self._needs_commit_queue, patches)
       
    88         if options.bugs:
       
    89             bugs_needing_cq = map(lambda patch: patch.bug_id(), patches_needing_cq)
       
    90             bugs_needing_cq = sorted(set(bugs_needing_cq))
       
    91             for bug_id in bugs_needing_cq:
       
    92                 print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
       
    93         else:
       
    94             for patch in patches_needing_cq:
       
    95                 print "%s" % tool.bugs.attachment_url_for_id(patch.id(), action="edit")
       
    96 
       
    97 
       
    98 class PatchesToReview(AbstractDeclarativeCommand):
       
    99     name = "patches-to-review"
       
   100     help_text = "List patches that are pending review"
       
   101 
       
   102     def execute(self, options, args, tool):
       
   103         patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue()
       
   104         log("Patches pending review:")
       
   105         for patch_id in patch_ids:
       
   106             print patch_id
       
   107 
       
   108 
       
   109 class LastGreenRevision(AbstractDeclarativeCommand):
       
   110     name = "last-green-revision"
       
   111     help_text = "Prints the last known good revision"
       
   112 
       
   113     def execute(self, options, args, tool):
       
   114         print self.tool.buildbot.last_green_revision()
       
   115 
       
   116 
       
   117 class WhatBroke(AbstractDeclarativeCommand):
       
   118     name = "what-broke"
       
   119     help_text = "Print failing buildbots (%s) and what revisions broke them" % BuildBot.default_host
       
   120 
       
   121     def _print_builder_line(self, builder_name, max_name_width, status_message):
       
   122         print "%s : %s" % (builder_name.ljust(max_name_width), status_message)
       
   123 
       
   124     # FIXME: This is slightly different from Builder.suspect_revisions_for_green_to_red_transition
       
   125     # due to needing to detect the "hit the limit" case an print a special message.
       
   126     def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True):
       
   127         builder = self.tool.buildbot.builder_with_name(builder_status["name"])
       
   128         red_build = builder.build(builder_status["build_number"])
       
   129         (last_green_build, first_red_build) = builder.find_failure_transition(red_build)
       
   130         if not first_red_build:
       
   131             self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)")
       
   132             return
       
   133         if not last_green_build:
       
   134             self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % first_red_build.revision())
       
   135             return
       
   136 
       
   137         suspect_revisions = range(first_red_build.revision(), last_green_build.revision(), -1)
       
   138         suspect_revisions.reverse()
       
   139         first_failure_message = ""
       
   140         if (first_red_build == builder.build(builder_status["build_number"])):
       
   141             first_failure_message = " FIRST FAILURE, possibly a flaky test"
       
   142         self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (suspect_revisions, first_failure_message))
       
   143         for revision in suspect_revisions:
       
   144             commit_info = self.tool.checkout().commit_info_for_revision(revision)
       
   145             if commit_info:
       
   146                 print commit_info.blame_string(self.tool.bugs)
       
   147             else:
       
   148                 print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
       
   149 
       
   150     def execute(self, options, args, tool):
       
   151         builder_statuses = tool.buildbot.builder_statuses()
       
   152         longest_builder_name = max(map(len, map(lambda builder: builder["name"], builder_statuses)))
       
   153         failing_builders = 0
       
   154         for builder_status in builder_statuses:
       
   155             # If the builder is green, print OK, exit.
       
   156             if builder_status["is_green"]:
       
   157                 continue
       
   158             self._print_blame_information_for_builder(builder_status, name_width=longest_builder_name)
       
   159             failing_builders += 1
       
   160         if failing_builders:
       
   161             print "%s of %s are failing" % (failing_builders, pluralize("builder", len(builder_statuses)))
       
   162         else:
       
   163             print "All builders are passing!"
       
   164 
       
   165 
       
   166 class WhoBrokeIt(AbstractDeclarativeCommand):
       
   167     name = "who-broke-it"
       
   168     help_text = "Print a list of revisions causing failures on %s" % BuildBot.default_host
       
   169 
       
   170     def execute(self, options, args, tool):
       
   171         for revision, builders in self.tool.buildbot.revisions_causing_failures(False).items():
       
   172             print "r%s appears to have broken %s" % (revision, [builder.name() for builder in builders])
       
   173 
       
   174 
       
   175 class ResultsFor(AbstractDeclarativeCommand):
       
   176     name = "results-for"
       
   177     help_text = "Print a list of failures for the passed revision from bots on %s" % BuildBot.default_host
       
   178     argument_names = "REVISION"
       
   179 
       
   180     def _print_layout_test_results(self, results):
       
   181         if not results:
       
   182             print " No results."
       
   183             return
       
   184         for title, files in results.parsed_results().items():
       
   185             print " %s" % title
       
   186             for filename in files:
       
   187                 print "  %s" % filename
       
   188 
       
   189     def execute(self, options, args, tool):
       
   190         builders = self.tool.buildbot.builders()
       
   191         for builder in builders:
       
   192             print "%s:" % builder.name()
       
   193             build = builder.build_for_revision(args[0], allow_failed_lookups=True)
       
   194             self._print_layout_test_results(build.layout_test_results())
       
   195 
       
   196 
       
   197 class FailureReason(AbstractDeclarativeCommand):
       
   198     name = "failure-reason"
       
   199     help_text = "Lists revisions where individual test failures started at %s" % BuildBot.default_host
       
   200 
       
   201     def _print_blame_information_for_transition(self, green_build, red_build, failing_tests):
       
   202         suspect_revisions = green_build.builder().suspect_revisions_for_transition(green_build, red_build)
       
   203         print "SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests)
       
   204         print "Suspect revisions:"
       
   205         for revision in suspect_revisions:
       
   206             commit_info = self.tool.checkout().commit_info_for_revision(revision)
       
   207             if commit_info:
       
   208                 print commit_info.blame_string(self.tool.bugs)
       
   209             else:
       
   210                 print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
       
   211 
       
   212     def _explain_failures_for_builder(self, builder, start_revision):
       
   213         print "Examining failures for \"%s\", starting at r%s" % (builder.name(), start_revision)
       
   214         revision_to_test = start_revision
       
   215         build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
       
   216         layout_test_results = build.layout_test_results()
       
   217         if not layout_test_results:
       
   218             # FIXME: This could be made more user friendly.
       
   219             print "Failed to load layout test results; can't continue. (start revision = r%s)" % start_revision
       
   220             return 1
       
   221 
       
   222         results_to_explain = set(layout_test_results.failing_tests())
       
   223         last_build_with_results = build
       
   224         print "Starting at %s" % revision_to_test
       
   225         while results_to_explain:
       
   226             revision_to_test -= 1
       
   227             new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
       
   228             if not new_build:
       
   229                 print "No build for %s" % revision_to_test
       
   230                 continue
       
   231             build = new_build
       
   232             latest_results = build.layout_test_results()
       
   233             if not latest_results:
       
   234                 print "No results build %s (r%s)" % (build._number, build.revision())
       
   235                 continue
       
   236             failures = set(latest_results.failing_tests())
       
   237             if len(failures) >= 20:
       
   238                 # FIXME: We may need to move this logic into the LayoutTestResults class.
       
   239                 # The buildbot stops runs after 20 failures so we don't have full results to work with here.
       
   240                 print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision())
       
   241                 continue
       
   242             fixed_results = results_to_explain - failures
       
   243             if not fixed_results:
       
   244                 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))
       
   245                 last_build_with_results = build
       
   246                 continue
       
   247             self._print_blame_information_for_transition(build, last_build_with_results, fixed_results)
       
   248             last_build_with_results = build
       
   249             results_to_explain -= fixed_results
       
   250         if results_to_explain:
       
   251             print "Failed to explain failures: %s" % results_to_explain
       
   252             return 1
       
   253         print "Explained all results for %s" % builder.name()
       
   254         return 0
       
   255 
       
   256     def _builder_to_explain(self):
       
   257         builder_statuses = self.tool.buildbot.builder_statuses()
       
   258         red_statuses = [status for status in builder_statuses if not status["is_green"]]
       
   259         print "%s failing" % (pluralize("builder", len(red_statuses)))
       
   260         builder_choices = [status["name"] for status in red_statuses]
       
   261         # We could offer an "All" choice here.
       
   262         chosen_name = User.prompt_with_list("Which builder to diagnose:", builder_choices)
       
   263         # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object.
       
   264         for status in red_statuses:
       
   265             if status["name"] == chosen_name:
       
   266                 return (self.tool.buildbot.builder_with_name(chosen_name), status["built_revision"])
       
   267 
       
   268     def execute(self, options, args, tool):
       
   269         (builder, latest_revision) = self._builder_to_explain()
       
   270         start_revision = self.tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision
       
   271         if not start_revision:
       
   272             print "Revision required."
       
   273             return 1
       
   274         return self._explain_failures_for_builder(builder, start_revision=int(start_revision))
       
   275 
       
   276 
       
   277 class TreeStatus(AbstractDeclarativeCommand):
       
   278     name = "tree-status"
       
   279     help_text = "Print the status of the %s buildbots" % BuildBot.default_host
       
   280     long_help = """Fetches build status from http://build.webkit.org/one_box_per_builder
       
   281 and displayes the status of each builder."""
       
   282 
       
   283     def execute(self, options, args, tool):
       
   284         for builder in tool.buildbot.builder_statuses():
       
   285             status_string = "ok" if builder["is_green"] else "FAIL"
       
   286             print "%s : %s" % (status_string.ljust(4), builder["name"])