diff options
author | Steve Block <steveblock@google.com> | 2010-04-27 16:31:00 +0100 |
---|---|---|
committer | Steve Block <steveblock@google.com> | 2010-05-11 14:42:12 +0100 |
commit | dcc8cf2e65d1aa555cce12431a16547e66b469ee (patch) | |
tree | 92a8d65cd5383bca9749f5327fb5e440563926e6 /WebKitTools/Scripts/webkitpy/tool/commands/queues.py | |
parent | ccac38a6b48843126402088a309597e682f40fe6 (diff) | |
download | external_webkit-dcc8cf2e65d1aa555cce12431a16547e66b469ee.zip external_webkit-dcc8cf2e65d1aa555cce12431a16547e66b469ee.tar.gz external_webkit-dcc8cf2e65d1aa555cce12431a16547e66b469ee.tar.bz2 |
Merge webkit.org at r58033 : Initial merge by git
Change-Id: If006c38561af287c50cd578d251629b51e4d8cd1
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/tool/commands/queues.py')
-rw-r--r-- | WebKitTools/Scripts/webkitpy/tool/commands/queues.py | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queues.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py new file mode 100644 index 0000000..f0da379 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py @@ -0,0 +1,364 @@ +# 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. + +import traceback +import os + +from datetime import datetime +from optparse import make_option +from StringIO import StringIO + +from webkitpy.common.net.bugzilla import CommitterValidator +from webkitpy.common.net.statusserver import StatusServer +from webkitpy.common.system.executive import ScriptError +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler +from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate +from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate +from webkitpy.tool.grammar import pluralize +from webkitpy.tool.multicommandtool import Command + +class AbstractQueue(Command, QueueEngineDelegate): + watchers = [ + "webkit-bot-watchers@googlegroups.com", + ] + + _pass_status = "Pass" + _fail_status = "Fail" + _error_status = "Error" + + def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations + options_list = (options or []) + [ + make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"), + make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."), + ] + Command.__init__(self, "Run the %s" % self.name, options=options_list) + self._iteration_count = 0 + + def _cc_watchers(self, bug_id): + try: + self.tool.bugs.add_cc_to_bug(bug_id, self.watchers) + except Exception, e: + traceback.print_exc() + log("Failed to CC watchers.") + + def run_webkit_patch(self, args): + webkit_patch_args = [self.tool.path()] + # FIXME: This is a hack, we should have a more general way to pass global options. + webkit_patch_args += ["--status-host=%s" % self.tool.status_server.host] + webkit_patch_args += map(str, args) + return self.tool.executive.run_and_throw_if_fail(webkit_patch_args) + + def _log_directory(self): + return "%s-logs" % self.name + + # QueueEngineDelegate methods + + def queue_log_path(self): + return os.path.join(self._log_directory(), "%s.log" % self.name) + + def work_item_log_path(self, work_item): + raise NotImplementedError, "subclasses must implement" + + def begin_work_queue(self): + log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root)) + if self.options.confirm: + response = self.tool.user.prompt("Are you sure? Type \"yes\" to continue: ") + if (response != "yes"): + error("User declined.") + log("Running WebKit %s." % self.name) + + def should_continue_work_queue(self): + self._iteration_count += 1 + return not self.options.iterations or self._iteration_count <= self.options.iterations + + def next_work_item(self): + raise NotImplementedError, "subclasses must implement" + + def should_proceed_with_work_item(self, work_item): + raise NotImplementedError, "subclasses must implement" + + def process_work_item(self, work_item): + raise NotImplementedError, "subclasses must implement" + + def handle_unexpected_error(self, work_item, message): + raise NotImplementedError, "subclasses must implement" + + # Command methods + + def execute(self, options, args, tool, engine=QueueEngine): + self.options = options + self.tool = tool + return engine(self.name, self, self.tool.wakeup_event).run() + + @classmethod + def _update_status_for_script_error(cls, tool, state, script_error, is_error=False): + message = script_error.message + if is_error: + message = "Error: %s" % message + output = script_error.message_with_output(output_limit=1024*1024) # 1MB + return tool.status_server.update_status(cls.name, message, state["patch"], StringIO(output)) + + +class AbstractPatchQueue(AbstractQueue): + def _update_status(self, message, patch=None, results_file=None): + self.tool.status_server.update_status(self.name, message, patch, results_file) + + def _did_pass(self, patch): + self._update_status(self._pass_status, patch) + + def _did_fail(self, patch): + self._update_status(self._fail_status, patch) + + def _did_error(self, patch, reason): + message = "%s: %s" % (self._error_status, reason) + self._update_status(message, patch) + + def work_item_log_path(self, patch): + return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) + + def log_progress(self, patch_ids): + log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids)))) + + +class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): + name = "commit-queue" + def __init__(self): + AbstractPatchQueue.__init__(self) + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractPatchQueue.begin_work_queue(self) + self.committer_validator = CommitterValidator(self.tool.bugs) + + def _validate_patches_in_commit_queue(self): + # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers. + bug_ids = self.tool.bugs.queries.fetch_bug_ids_from_commit_queue() + all_patches = sum([self.tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) for bug_id in bug_ids], []) + valid_patches = self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches) + if not self._builders_are_green(): + return filter(lambda patch: patch.is_rollout(), valid_patches) + return valid_patches + + def next_work_item(self): + patches = self._validate_patches_in_commit_queue() + # FIXME: We could sort the patches in a specific order here, was suggested by https://bugs.webkit.org/show_bug.cgi?id=33395 + if not patches: + self._update_status("Empty queue") + return None + # Only bother logging if we have patches in the queue. + self.log_progress([patch.id() for patch in patches]) + return patches[0] + + def _can_build_and_test(self): + try: + self.run_webkit_patch([ + "build-and-test", + "--force-clean", + "--build", + "--test", + "--non-interactive", + "--no-update", + "--build-style=both", + "--quiet"]) + except ScriptError, e: + self._update_status("Unable to successfully build and test", None) + return False + return True + + def _builders_are_green(self): + red_builders_names = self.tool.buildbot.red_core_builders_names() + if red_builders_names: + red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names. + self._update_status("Builders [%s] are red. See http://build.webkit.org" % ", ".join(red_builders_names), None) + return False + return True + + def should_proceed_with_work_item(self, patch): + if not patch.is_rollout(): + if not self._builders_are_green(): + return False + self._update_status("Landing patch", patch) + return True + + def _land(self, patch, first_run=False): + try: + # We need to check the builders, unless we're trying to land a + # rollout (in which case the builders are probably red.) + if not patch.is_rollout() and not self._builders_are_green(): + # We return true here because we want to return to the main + # QueueEngine loop as quickly as possible. + return True + args = [ + "land-attachment", + "--force-clean", + "--build", + "--test", + "--non-interactive", + # The master process is responsible for checking the status + # of the builders (see above call to _builders_are_green). + "--ignore-builders", + "--build-style=both", + "--quiet", + patch.id() + ] + if not first_run: + # The first time through, we don't reject the patch from the + # commit queue because we want to make sure we can build and + # test ourselves. However, the second time through, we + # register ourselves as the parent-command so we can reject + # the patch on failure. + args.append("--parent-command=commit-queue") + # The second time through, we also don't want to update so we + # know we're testing the same revision that we successfully + # built and tested. + args.append("--no-update") + self.run_webkit_patch(args) + self._did_pass(patch) + return True + except ScriptError, e: + if first_run: + return False + self._did_fail(patch) + raise + + def process_work_item(self, patch): + self._cc_watchers(patch.bug_id()) + if not self._land(patch, first_run=True): + # The patch failed to land, but the bots were green. It's possible + # that the bots were behind. To check that case, we try to build and + # test ourselves. + if not self._can_build_and_test(): + return False + # Hum, looks like the patch is actually bad. Of course, we could + # have been bitten by a flaky test the first time around. We try + # to land again. If it fails a second time, we're pretty sure its + # a bad test and re can reject it outright. + self._land(patch) + return True + + def handle_unexpected_error(self, patch, message): + self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) + + # StepSequenceErrorHandler methods + + @staticmethod + def _error_message_for_bug(tool, status_id, script_error): + if not script_error.output: + return script_error.message_with_output() + results_link = tool.status_server.results_url_for_status(status_id) + return "%s\nFull output: %s" % (script_error.message_with_output(), results_link) + + @classmethod + def handle_script_error(cls, tool, state, script_error): + status_id = cls._update_status_for_script_error(tool, state, script_error) + validator = CommitterValidator(tool.bugs) + validator.reject_patch_from_commit_queue(state["patch"].id(), cls._error_message_for_bug(tool, status_id, script_error)) + + +class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler): + def __init__(self, options=None): + AbstractPatchQueue.__init__(self, options) + + def review_patch(self, patch): + raise NotImplementedError, "subclasses must implement" + + # PersistentPatchCollectionDelegate methods + + def collection_name(self): + return self.name + + def fetch_potential_patch_ids(self): + return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue() + + def status_server(self): + return self.tool.status_server + + def is_terminal_status(self, status): + return status == "Pass" or status == "Fail" or status.startswith("Error:") + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractPatchQueue.begin_work_queue(self) + self._patches = PersistentPatchCollection(self) + + def next_work_item(self): + patch_id = self._patches.next() + if patch_id: + return self.tool.bugs.fetch_attachment(patch_id) + self._update_status("Empty queue") + + def should_proceed_with_work_item(self, patch): + raise NotImplementedError, "subclasses must implement" + + def process_work_item(self, patch): + try: + if not self.review_patch(patch): + return False + self._did_pass(patch) + return True + except ScriptError, e: + if e.exit_code != QueueEngine.handled_error_code: + self._did_fail(patch) + raise e + + def handle_unexpected_error(self, patch, message): + log(message) + + # StepSequenceErrorHandler methods + + @classmethod + def handle_script_error(cls, tool, state, script_error): + log(script_error.message_with_output()) + + +class StyleQueue(AbstractReviewQueue): + name = "style-queue" + def __init__(self): + AbstractReviewQueue.__init__(self) + + def should_proceed_with_work_item(self, patch): + self._update_status("Checking style", patch) + return True + + def review_patch(self, patch): + self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()]) + return True + + @classmethod + def handle_script_error(cls, tool, state, script_error): + is_svn_apply = script_error.command_name() == "svn-apply" + status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply) + if is_svn_apply: + QueueEngine.exit_after_handled_error(script_error) + 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)) + tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers) + exit(1) |