|
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 import traceback |
|
31 import os |
|
32 |
|
33 from datetime import datetime |
|
34 from optparse import make_option |
|
35 from StringIO import StringIO |
|
36 |
|
37 from webkitpy.common.net.bugzilla import CommitterValidator |
|
38 from webkitpy.common.net.statusserver import StatusServer |
|
39 from webkitpy.common.system.executive import ScriptError |
|
40 from webkitpy.common.system.deprecated_logging import error, log |
|
41 from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler |
|
42 from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate |
|
43 from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate |
|
44 from webkitpy.tool.grammar import pluralize |
|
45 from webkitpy.tool.multicommandtool import Command, TryAgain |
|
46 |
|
47 class AbstractQueue(Command, QueueEngineDelegate): |
|
48 watchers = [ |
|
49 ] |
|
50 |
|
51 _pass_status = "Pass" |
|
52 _fail_status = "Fail" |
|
53 _error_status = "Error" |
|
54 |
|
55 def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations |
|
56 options_list = (options or []) + [ |
|
57 make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"), |
|
58 make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."), |
|
59 ] |
|
60 Command.__init__(self, "Run the %s" % self.name, options=options_list) |
|
61 self._iteration_count = 0 |
|
62 |
|
63 def _cc_watchers(self, bug_id): |
|
64 try: |
|
65 self.tool.bugs.add_cc_to_bug(bug_id, self.watchers) |
|
66 except Exception, e: |
|
67 traceback.print_exc() |
|
68 log("Failed to CC watchers.") |
|
69 |
|
70 def run_webkit_patch(self, args): |
|
71 webkit_patch_args = [self.tool.path()] |
|
72 # FIXME: This is a hack, we should have a more general way to pass global options. |
|
73 # FIXME: We must always pass global options and their value in one argument |
|
74 # because our global option code looks for the first argument which does |
|
75 # not begin with "-" and assumes that is the command name. |
|
76 webkit_patch_args += ["--status-host=%s" % self.tool.status_server.host] |
|
77 webkit_patch_args.extend(args) |
|
78 return self.tool.executive.run_and_throw_if_fail(webkit_patch_args) |
|
79 |
|
80 def _log_directory(self): |
|
81 return "%s-logs" % self.name |
|
82 |
|
83 # QueueEngineDelegate methods |
|
84 |
|
85 def queue_log_path(self): |
|
86 return os.path.join(self._log_directory(), "%s.log" % self.name) |
|
87 |
|
88 def work_item_log_path(self, work_item): |
|
89 raise NotImplementedError, "subclasses must implement" |
|
90 |
|
91 def begin_work_queue(self): |
|
92 log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root)) |
|
93 if self.options.confirm: |
|
94 response = self.tool.user.prompt("Are you sure? Type \"yes\" to continue: ") |
|
95 if (response != "yes"): |
|
96 error("User declined.") |
|
97 log("Running WebKit %s." % self.name) |
|
98 |
|
99 def should_continue_work_queue(self): |
|
100 self._iteration_count += 1 |
|
101 return not self.options.iterations or self._iteration_count <= self.options.iterations |
|
102 |
|
103 def next_work_item(self): |
|
104 raise NotImplementedError, "subclasses must implement" |
|
105 |
|
106 def should_proceed_with_work_item(self, work_item): |
|
107 raise NotImplementedError, "subclasses must implement" |
|
108 |
|
109 def process_work_item(self, work_item): |
|
110 raise NotImplementedError, "subclasses must implement" |
|
111 |
|
112 def handle_unexpected_error(self, work_item, message): |
|
113 raise NotImplementedError, "subclasses must implement" |
|
114 |
|
115 # Command methods |
|
116 |
|
117 def execute(self, options, args, tool, engine=QueueEngine): |
|
118 self.options = options |
|
119 self.tool = tool |
|
120 return engine(self.name, self, self.tool.wakeup_event).run() |
|
121 |
|
122 @classmethod |
|
123 def _update_status_for_script_error(cls, tool, state, script_error, is_error=False): |
|
124 message = str(script_error) |
|
125 if is_error: |
|
126 message = "Error: %s" % message |
|
127 output = script_error.message_with_output(output_limit=1024*1024) # 1MB |
|
128 # We pre-encode the string to a byte array before passing it |
|
129 # to status_server, because ClientForm (part of mechanize) |
|
130 # wants a file-like object with pre-encoded data. |
|
131 return tool.status_server.update_status(cls.name, message, state["patch"], StringIO(output.encode("utf-8"))) |
|
132 |
|
133 |
|
134 class AbstractPatchQueue(AbstractQueue): |
|
135 def _update_status(self, message, patch=None, results_file=None): |
|
136 self.tool.status_server.update_status(self.name, message, patch, results_file) |
|
137 |
|
138 def _update_work_items(self, patch_ids): |
|
139 self.tool.status_server.update_work_items(self.name, patch_ids) |
|
140 |
|
141 def _did_pass(self, patch): |
|
142 self._update_status(self._pass_status, patch) |
|
143 |
|
144 def _did_fail(self, patch): |
|
145 self._update_status(self._fail_status, patch) |
|
146 |
|
147 def _did_error(self, patch, reason): |
|
148 message = "%s: %s" % (self._error_status, reason) |
|
149 self._update_status(message, patch) |
|
150 |
|
151 def work_item_log_path(self, patch): |
|
152 return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) |
|
153 |
|
154 def log_progress(self, patch_ids): |
|
155 log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids)))) |
|
156 |
|
157 |
|
158 class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): |
|
159 name = "commit-queue" |
|
160 def __init__(self): |
|
161 AbstractPatchQueue.__init__(self) |
|
162 |
|
163 # AbstractPatchQueue methods |
|
164 |
|
165 def begin_work_queue(self): |
|
166 AbstractPatchQueue.begin_work_queue(self) |
|
167 self.committer_validator = CommitterValidator(self.tool.bugs) |
|
168 |
|
169 def _validate_patches_in_commit_queue(self): |
|
170 # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers. |
|
171 bug_ids = self.tool.bugs.queries.fetch_bug_ids_from_commit_queue() |
|
172 all_patches = sum([self.tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) for bug_id in bug_ids], []) |
|
173 return self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches) |
|
174 |
|
175 def _patch_cmp(self, a, b): |
|
176 # Sort first by is_rollout, then by attach_date. |
|
177 # Reversing the order so that is_rollout is first. |
|
178 rollout_cmp = cmp(b.is_rollout(), a.is_rollout()) |
|
179 if (rollout_cmp != 0): |
|
180 return rollout_cmp |
|
181 return cmp(a.attach_date(), b.attach_date()) |
|
182 |
|
183 def next_work_item(self): |
|
184 patches = self._validate_patches_in_commit_queue() |
|
185 patches = sorted(patches, self._patch_cmp) |
|
186 self._update_work_items([patch.id() for patch in patches]) |
|
187 if not patches: |
|
188 self._update_status("Empty queue") |
|
189 return None |
|
190 # Only bother logging if we have patches in the queue. |
|
191 self.log_progress([patch.id() for patch in patches]) |
|
192 return patches[0] |
|
193 |
|
194 def _can_build_and_test(self): |
|
195 try: |
|
196 self.run_webkit_patch([ |
|
197 "build-and-test", |
|
198 "--force-clean", |
|
199 "--build", |
|
200 "--test", |
|
201 "--non-interactive", |
|
202 "--no-update", |
|
203 "--build-style=both", |
|
204 "--quiet"]) |
|
205 except ScriptError, e: |
|
206 self._update_status("Unable to successfully build and test", None) |
|
207 return False |
|
208 return True |
|
209 |
|
210 def should_proceed_with_work_item(self, patch): |
|
211 patch_text = "rollout patch" if patch.is_rollout() else "patch" |
|
212 self._update_status("Landing %s" % patch_text, patch) |
|
213 return True |
|
214 |
|
215 def _land(self, patch, first_run=False): |
|
216 try: |
|
217 args = [ |
|
218 "land-attachment", |
|
219 "--force-clean", |
|
220 "--build", |
|
221 "--non-interactive", |
|
222 "--ignore-builders", |
|
223 "--build-style=both", |
|
224 "--quiet", |
|
225 patch.id() |
|
226 ] |
|
227 # We don't bother to run tests for rollouts as that makes them too slow. |
|
228 if not patch.is_rollout(): |
|
229 args.append("--test") |
|
230 if not first_run: |
|
231 # The first time through, we don't reject the patch from the |
|
232 # commit queue because we want to make sure we can build and |
|
233 # test ourselves. However, the second time through, we |
|
234 # register ourselves as the parent-command so we can reject |
|
235 # the patch on failure. |
|
236 args.append("--parent-command=commit-queue") |
|
237 # The second time through, we also don't want to update so we |
|
238 # know we're testing the same revision that we successfully |
|
239 # built and tested. |
|
240 args.append("--no-update") |
|
241 self.run_webkit_patch(args) |
|
242 self._did_pass(patch) |
|
243 return True |
|
244 except ScriptError, e: |
|
245 if first_run: |
|
246 return False |
|
247 self._did_fail(patch) |
|
248 raise |
|
249 |
|
250 def process_work_item(self, patch): |
|
251 self._cc_watchers(patch.bug_id()) |
|
252 if not self._land(patch, first_run=True): |
|
253 # The patch failed to land, but the bots were green. It's possible |
|
254 # that the bots were behind. To check that case, we try to build and |
|
255 # test ourselves. |
|
256 if not self._can_build_and_test(): |
|
257 return False |
|
258 # Hum, looks like the patch is actually bad. Of course, we could |
|
259 # have been bitten by a flaky test the first time around. We try |
|
260 # to land again. If it fails a second time, we're pretty sure its |
|
261 # a bad test and re can reject it outright. |
|
262 self._land(patch) |
|
263 return True |
|
264 |
|
265 def handle_unexpected_error(self, patch, message): |
|
266 self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) |
|
267 |
|
268 # StepSequenceErrorHandler methods |
|
269 @staticmethod |
|
270 def _error_message_for_bug(tool, status_id, script_error): |
|
271 if not script_error.output: |
|
272 return script_error.message_with_output() |
|
273 results_link = tool.status_server.results_url_for_status(status_id) |
|
274 return "%s\nFull output: %s" % (script_error.message_with_output(), results_link) |
|
275 |
|
276 @classmethod |
|
277 def handle_script_error(cls, tool, state, script_error): |
|
278 status_id = cls._update_status_for_script_error(tool, state, script_error) |
|
279 validator = CommitterValidator(tool.bugs) |
|
280 validator.reject_patch_from_commit_queue(state["patch"].id(), cls._error_message_for_bug(tool, status_id, script_error)) |
|
281 |
|
282 @classmethod |
|
283 def handle_checkout_needs_update(cls, tool, state, options, error): |
|
284 # The only time when we find out that out checkout needs update is |
|
285 # when we were ready to actually pull the trigger and land the patch. |
|
286 # Rather than spinning in the master process, we retry without |
|
287 # building or testing, which is much faster. |
|
288 options.build = False |
|
289 options.test = False |
|
290 options.update = True |
|
291 raise TryAgain() |
|
292 |
|
293 |
|
294 class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler): |
|
295 name = "rietveld-upload-queue" |
|
296 |
|
297 def __init__(self): |
|
298 AbstractPatchQueue.__init__(self) |
|
299 |
|
300 # AbstractPatchQueue methods |
|
301 |
|
302 def next_work_item(self): |
|
303 patch_id = self.tool.bugs.queries.fetch_first_patch_from_rietveld_queue() |
|
304 if patch_id: |
|
305 return patch_id |
|
306 self._update_status("Empty queue") |
|
307 |
|
308 def should_proceed_with_work_item(self, patch): |
|
309 self._update_status("Uploading patch", patch) |
|
310 return True |
|
311 |
|
312 def process_work_item(self, patch): |
|
313 try: |
|
314 self.run_webkit_patch(["post-attachment-to-rietveld", "--force-clean", "--non-interactive", "--parent-command=rietveld-upload-queue", patch.id()]) |
|
315 self._did_pass(patch) |
|
316 return True |
|
317 except ScriptError, e: |
|
318 if e.exit_code != QueueEngine.handled_error_code: |
|
319 self._did_fail(patch) |
|
320 raise e |
|
321 |
|
322 @classmethod |
|
323 def _reject_patch(cls, tool, patch_id): |
|
324 tool.bugs.set_flag_on_attachment(patch_id, "in-rietveld", "-") |
|
325 |
|
326 def handle_unexpected_error(self, patch, message): |
|
327 log(message) |
|
328 self._reject_patch(self.tool, patch.id()) |
|
329 |
|
330 # StepSequenceErrorHandler methods |
|
331 |
|
332 @classmethod |
|
333 def handle_script_error(cls, tool, state, script_error): |
|
334 log(script_error.message_with_output()) |
|
335 cls._update_status_for_script_error(tool, state, script_error) |
|
336 cls._reject_patch(tool, state["patch"].id()) |
|
337 |
|
338 |
|
339 class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler): |
|
340 def __init__(self, options=None): |
|
341 AbstractPatchQueue.__init__(self, options) |
|
342 |
|
343 def review_patch(self, patch): |
|
344 raise NotImplementedError, "subclasses must implement" |
|
345 |
|
346 # PersistentPatchCollectionDelegate methods |
|
347 |
|
348 def collection_name(self): |
|
349 return self.name |
|
350 |
|
351 def fetch_potential_patch_ids(self): |
|
352 return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue() |
|
353 |
|
354 def status_server(self): |
|
355 return self.tool.status_server |
|
356 |
|
357 def is_terminal_status(self, status): |
|
358 return status == "Pass" or status == "Fail" or status.startswith("Error:") |
|
359 |
|
360 # AbstractPatchQueue methods |
|
361 |
|
362 def begin_work_queue(self): |
|
363 AbstractPatchQueue.begin_work_queue(self) |
|
364 self._patches = PersistentPatchCollection(self) |
|
365 |
|
366 def next_work_item(self): |
|
367 patch_id = self._patches.next() |
|
368 if patch_id: |
|
369 return self.tool.bugs.fetch_attachment(patch_id) |
|
370 self._update_status("Empty queue") |
|
371 |
|
372 def should_proceed_with_work_item(self, patch): |
|
373 raise NotImplementedError, "subclasses must implement" |
|
374 |
|
375 def process_work_item(self, patch): |
|
376 try: |
|
377 if not self.review_patch(patch): |
|
378 return False |
|
379 self._did_pass(patch) |
|
380 return True |
|
381 except ScriptError, e: |
|
382 if e.exit_code != QueueEngine.handled_error_code: |
|
383 self._did_fail(patch) |
|
384 raise e |
|
385 |
|
386 def handle_unexpected_error(self, patch, message): |
|
387 log(message) |
|
388 |
|
389 # StepSequenceErrorHandler methods |
|
390 |
|
391 @classmethod |
|
392 def handle_script_error(cls, tool, state, script_error): |
|
393 log(script_error.message_with_output()) |
|
394 |
|
395 |
|
396 class StyleQueue(AbstractReviewQueue): |
|
397 name = "style-queue" |
|
398 def __init__(self): |
|
399 AbstractReviewQueue.__init__(self) |
|
400 |
|
401 def should_proceed_with_work_item(self, patch): |
|
402 self._update_status("Checking style", patch) |
|
403 return True |
|
404 |
|
405 def review_patch(self, patch): |
|
406 self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()]) |
|
407 return True |
|
408 |
|
409 @classmethod |
|
410 def handle_script_error(cls, tool, state, script_error): |
|
411 is_svn_apply = script_error.command_name() == "svn-apply" |
|
412 status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply) |
|
413 if is_svn_apply: |
|
414 QueueEngine.exit_after_handled_error(script_error) |
|
415 message = "Attachment %s did not pass %s:\n\n%s\n\nIf any of these errors are false positives, please file a bug against check-webkit-style." % (state["patch"].id(), cls.name, script_error.message_with_output(output_limit=3*1024)) |
|
416 tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers) |
|
417 exit(1) |