1 # Copyright (c) 2009 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
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
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.
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.
30 from __future__ import with_statement
37 from datetime import datetime
38 from optparse import make_option
39 from StringIO import StringIO
41 from webkitpy.common.config.committervalidator import CommitterValidator
42 from webkitpy.common.net.bugzilla import Attachment
43 from webkitpy.common.net.statusserver import StatusServer
44 from webkitpy.common.system.deprecated_logging import error, log
45 from webkitpy.common.system.executive import ScriptError
46 from webkitpy.tool.bot.botinfo import BotInfo
47 from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate
48 from webkitpy.tool.bot.expectedfailures import ExpectedFailures
49 from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder
50 from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader
51 from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
52 from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter
53 from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
54 from webkitpy.tool.multicommandtool import Command, TryAgain
57 class AbstractQueue(Command, QueueEngineDelegate):
63 _retry_status = "Retry"
64 _error_status = "Error"
66 def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
67 options_list = (options or []) + [
68 make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"),
69 make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."),
71 Command.__init__(self, "Run the %s" % self.name, options=options_list)
72 self._iteration_count = 0
74 def _cc_watchers(self, bug_id):
76 self._tool.bugs.add_cc_to_bug(bug_id, self.watchers)
79 log("Failed to CC watchers.")
81 def run_webkit_patch(self, args):
82 webkit_patch_args = [self._tool.path()]
83 # FIXME: This is a hack, we should have a more general way to pass global options.
84 # FIXME: We must always pass global options and their value in one argument
85 # because our global option code looks for the first argument which does
86 # not begin with "-" and assumes that is the command name.
87 webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host]
88 if self._tool.status_server.bot_id:
89 webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id]
90 if self._options.port:
91 webkit_patch_args += ["--port=%s" % self._options.port]
92 webkit_patch_args.extend(args)
93 # FIXME: There is probably no reason to use run_and_throw_if_fail anymore.
94 # run_and_throw_if_fail was invented to support tee'd output
95 # (where we write both to a log file and to the console at once),
96 # but the queues don't need live-progress, a dump-of-output at the
97 # end should be sufficient.
98 return self._tool.executive.run_and_throw_if_fail(webkit_patch_args, cwd=self._tool.scm().checkout_root)
100 def _log_directory(self):
101 return os.path.join("..", "%s-logs" % self.name)
103 # QueueEngineDelegate methods
105 def queue_log_path(self):
106 return os.path.join(self._log_directory(), "%s.log" % self.name)
108 def work_item_log_path(self, work_item):
109 raise NotImplementedError, "subclasses must implement"
111 def begin_work_queue(self):
112 log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root))
113 if self._options.confirm:
114 response = self._tool.user.prompt("Are you sure? Type \"yes\" to continue: ")
115 if (response != "yes"):
116 error("User declined.")
117 log("Running WebKit %s." % self.name)
118 self._tool.status_server.update_status(self.name, "Starting Queue")
120 def stop_work_queue(self, reason):
121 self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason)
123 def should_continue_work_queue(self):
124 self._iteration_count += 1
125 return not self._options.iterations or self._iteration_count <= self._options.iterations
127 def next_work_item(self):
128 raise NotImplementedError, "subclasses must implement"
130 def should_proceed_with_work_item(self, work_item):
131 raise NotImplementedError, "subclasses must implement"
133 def process_work_item(self, work_item):
134 raise NotImplementedError, "subclasses must implement"
136 def handle_unexpected_error(self, work_item, message):
137 raise NotImplementedError, "subclasses must implement"
141 def execute(self, options, args, tool, engine=QueueEngine):
142 self._options = options # FIXME: This code is wrong. Command.options is a list, this assumes an Options element!
143 self._tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this!
144 return engine(self.name, self, self._tool.wakeup_event).run()
147 def _log_from_script_error_for_upload(cls, script_error, output_limit=None):
148 # We have seen request timeouts with app engine due to large
149 # log uploads. Trying only the last 512k.
151 output_limit = 512 * 1024 # 512k
152 output = script_error.message_with_output(output_limit=output_limit)
153 # We pre-encode the string to a byte array before passing it
154 # to status_server, because ClientForm (part of mechanize)
155 # wants a file-like object with pre-encoded data.
156 return StringIO(output.encode("utf-8"))
159 def _update_status_for_script_error(cls, tool, state, script_error, is_error=False):
160 message = str(script_error)
162 message = "Error: %s" % message
163 failure_log = cls._log_from_script_error_for_upload(script_error)
164 return tool.status_server.update_status(cls.name, message, state["patch"], failure_log)
167 class FeederQueue(AbstractQueue):
168 name = "feeder-queue"
170 _sleep_duration = 30 # seconds
172 # AbstractQueue methods
174 def begin_work_queue(self):
175 AbstractQueue.begin_work_queue(self)
177 CommitQueueFeeder(self._tool),
178 EWSFeeder(self._tool),
181 def next_work_item(self):
182 # This really show inherit from some more basic class that doesn't
183 # understand work items, but the base class in the heirarchy currently
184 # understands work items.
185 return "synthetic-work-item"
187 def should_proceed_with_work_item(self, work_item):
190 def process_work_item(self, work_item):
191 for feeder in self.feeders:
193 time.sleep(self._sleep_duration)
196 def work_item_log_path(self, work_item):
199 def handle_unexpected_error(self, work_item, message):
203 class AbstractPatchQueue(AbstractQueue):
204 def _update_status(self, message, patch=None, results_file=None):
205 return self._tool.status_server.update_status(self.name, message, patch, results_file)
207 def _next_patch(self):
208 patch_id = self._tool.status_server.next_work_item(self.name)
211 patch = self._tool.bugs.fetch_attachment(patch_id)
213 # FIXME: Using a fake patch because release_work_item has the wrong API.
214 # We also don't really need to release the lock (although that's fine),
215 # mostly we just need to remove this bogus patch from our queue.
216 # If for some reason bugzilla is just down, then it will be re-fed later.
217 patch = Attachment({'id': patch_id}, None)
218 self._release_work_item(patch)
222 def _release_work_item(self, patch):
223 self._tool.status_server.release_work_item(self.name, patch)
225 def _did_pass(self, patch):
226 self._update_status(self._pass_status, patch)
227 self._release_work_item(patch)
229 def _did_fail(self, patch):
230 self._update_status(self._fail_status, patch)
231 self._release_work_item(patch)
233 def _did_retry(self, patch):
234 self._update_status(self._retry_status, patch)
235 self._release_work_item(patch)
237 def _did_error(self, patch, reason):
238 message = "%s: %s" % (self._error_status, reason)
239 self._update_status(message, patch)
240 self._release_work_item(patch)
242 # FIXME: This probably belongs at a layer below AbstractPatchQueue, but shared by CommitQueue and the EarlyWarningSystem.
243 def _upload_results_archive_for_patch(self, patch, results_archive_zip):
244 bot_id = self._tool.status_server.bot_id or "bot"
245 description = "Archive of layout-test-results from %s" % bot_id
246 # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading.
247 results_archive_file = results_archive_zip.fp
248 # Rewind the file object to start (since Mechanize won't do that automatically)
249 # See https://bugs.webkit.org/show_bug.cgi?id=54593
250 results_archive_file.seek(0)
251 # FIXME: This is a small lie to always say run-webkit-tests since Chromium uses new-run-webkit-tests.
252 # We could make this code look up the test script name off the port.
253 comment_text = "The attached test failures were seen while running run-webkit-tests on the %s.\n" % (self.name)
254 # FIXME: We could easily list the test failures from the archive here,
255 # currently callers do that separately.
256 comment_text += BotInfo(self._tool).summary_text()
257 self._tool.bugs.add_attachment_to_bug(patch.bug_id(), results_archive_file, description, filename="layout-test-results.zip", comment_text=comment_text)
259 def work_item_log_path(self, patch):
260 return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
263 class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate):
264 name = "commit-queue"
266 # AbstractPatchQueue methods
268 def begin_work_queue(self):
269 AbstractPatchQueue.begin_work_queue(self)
270 self.committer_validator = CommitterValidator(self._tool.bugs)
271 self._expected_failures = ExpectedFailures()
272 self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory())
274 def next_work_item(self):
275 return self._next_patch()
277 def should_proceed_with_work_item(self, patch):
278 patch_text = "rollout patch" if patch.is_rollout() else "patch"
279 self._update_status("Processing %s" % patch_text, patch)
282 def process_work_item(self, patch):
283 self._cc_watchers(patch.bug_id())
284 task = CommitQueueTask(self, patch)
287 self._did_pass(patch)
289 self._did_retry(patch)
290 except ScriptError, e:
291 validator = CommitterValidator(self._tool.bugs)
292 validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e))
293 results_archive = task.results_archive_from_patch_test_run(patch)
295 self._upload_results_archive_for_patch(patch, results_archive)
296 self._did_fail(patch)
298 def _error_message_for_bug(self, status_id, script_error):
299 if not script_error.output:
300 return script_error.message_with_output()
301 results_link = self._tool.status_server.results_url_for_status(status_id)
302 return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
304 def handle_unexpected_error(self, patch, message):
305 self.committer_validator.reject_patch_from_commit_queue(patch.id(), message)
307 # CommitQueueTaskDelegate methods
309 def run_command(self, command):
310 self.run_webkit_patch(command)
312 def command_passed(self, message, patch):
313 self._update_status(message, patch=patch)
315 def command_failed(self, message, script_error, patch):
316 failure_log = self._log_from_script_error_for_upload(script_error)
317 return self._update_status(message, patch=patch, results_file=failure_log)
319 def expected_failures(self):
320 return self._expected_failures
322 def layout_test_results(self):
323 return self._layout_test_results_reader.results()
325 def archive_last_layout_test_results(self, patch):
326 return self._layout_test_results_reader.archive(patch)
328 def build_style(self):
331 def refetch_patch(self, patch):
332 return self._tool.bugs.fetch_attachment(patch.id())
334 def report_flaky_tests(self, patch, flaky_test_results, results_archive=None):
335 reporter = FlakyTestReporter(self._tool, self.name)
336 reporter.report_flaky_tests(patch, flaky_test_results, results_archive)
338 # StepSequenceErrorHandler methods
340 def handle_script_error(cls, tool, state, script_error):
341 # Hitting this error handler should be pretty rare. It does occur,
342 # however, when a patch no longer applies to top-of-tree in the final
344 log(script_error.message_with_output())
347 def handle_checkout_needs_update(cls, tool, state, options, error):
348 message = "Tests passed, but commit failed (checkout out of date). Updating, then landing without building or re-running tests."
349 tool.status_server.update_status(cls.name, message, state["patch"])
350 # The only time when we find out that out checkout needs update is
351 # when we were ready to actually pull the trigger and land the patch.
352 # Rather than spinning in the master process, we retry without
353 # building or testing, which is much faster.
354 options.build = False
356 options.update = True
360 class AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler):
361 """This is the base-class for the EWS queues and the style-queue."""
362 def __init__(self, options=None):
363 AbstractPatchQueue.__init__(self, options)
365 def review_patch(self, patch):
366 raise NotImplementedError("subclasses must implement")
368 # AbstractPatchQueue methods
370 def begin_work_queue(self):
371 AbstractPatchQueue.begin_work_queue(self)
373 def next_work_item(self):
374 return self._next_patch()
376 def should_proceed_with_work_item(self, patch):
377 raise NotImplementedError("subclasses must implement")
379 def process_work_item(self, patch):
381 if not self.review_patch(patch):
383 self._did_pass(patch)
385 except ScriptError, e:
386 if e.exit_code != QueueEngine.handled_error_code:
387 self._did_fail(patch)
389 # The subprocess handled the error, but won't have released the patch, so we do.
390 # FIXME: We need to simplify the rules by which _release_work_item is called.
391 self._release_work_item(patch)
394 def handle_unexpected_error(self, patch, message):
397 # StepSequenceErrorHandler methods
400 def handle_script_error(cls, tool, state, script_error):
401 log(script_error.message_with_output())
404 class StyleQueue(AbstractReviewQueue):
407 AbstractReviewQueue.__init__(self)
409 def should_proceed_with_work_item(self, patch):
410 self._update_status("Checking style", patch)
413 def review_patch(self, patch):
414 self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()])
418 def handle_script_error(cls, tool, state, script_error):
419 is_svn_apply = script_error.command_name() == "svn-apply"
420 status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
422 QueueEngine.exit_after_handled_error(script_error)
423 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))
424 tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers)