|
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"]) |