2009-12-06 Adam Barth <abarth@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / modules / commands / queues.py
1 #!/usr/bin/env python
2 # Copyright (c) 2009, Google Inc. All rights reserved.
3 # Copyright (c) 2009 Apple Inc. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8
9 #     * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 #     * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
14 # distribution.
15 #     * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
18
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31 import re
32
33 from datetime import datetime
34 from optparse import make_option
35
36 from modules.grammar import pluralize
37 from modules.landingsequence import LandingSequence, ConditionalLandingSequence, LandingSequenceErrorHandler
38 from modules.logging import error, log
39 from modules.multicommandtool import Command
40 from modules.patchcollection import PatchCollection, PersistentPatchCollection, PersistentPatchCollectionDelegate
41 from modules.processutils import run_and_throw_if_fail
42 from modules.scm import ScriptError
43 from modules.statusbot import StatusBot
44 from modules.workqueue import WorkQueue, WorkQueueDelegate
45
46 class AbstractQueue(Command, WorkQueueDelegate):
47     show_in_main_help = False
48     watchers = "webkit-bot-watchers@googlegroups.com"
49     def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
50         options_list = (options or []) + [
51             make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
52             make_option("--status-host", action="store", type="string", dest="status_host", default=StatusBot.default_host, help="Hostname (e.g. localhost or commit.webkit.org) where status updates should be posted."),
53         ]
54         Command.__init__(self, "Run the %s" % self.name, options=options_list)
55
56     def _cc_watchers(self, bug_id):
57         try:
58             self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
59         except Exception, e:
60             log("Failed to CC watchers: %s." % e)
61
62     def queue_log_path(self):
63         return "%s.log" % self.name
64
65     def work_logs_directory(self):
66         return "%s-logs" % self.name
67
68     def status_host(self):
69         return self.options.status_host
70
71     def begin_work_queue(self):
72         log("CAUTION: %s will discard all local changes in %s" % (self.name, self.tool.scm().checkout_root))
73         if self.options.confirm:
74             response = raw_input("Are you sure?  Type \"yes\" to continue: ")
75             if (response != "yes"):
76                 error("User declined.")
77         log("Running WebKit %s. %s" % (self.name, datetime.now().strftime(WorkQueue.log_date_format)))
78
79     def should_continue_work_queue(self):
80         return True
81
82     def next_work_item(self):
83         raise NotImplementedError, "subclasses must implement"
84
85     def should_proceed_with_work_item(self, work_item):
86         raise NotImplementedError, "subclasses must implement"
87
88     def process_work_item(self, work_item):
89         raise NotImplementedError, "subclasses must implement"
90
91     def handle_unexpected_error(self, work_item, message):
92         raise NotImplementedError, "subclasses must implement"
93
94     def run_bugzilla_tool(self, args):
95         bugzilla_tool_args = [self.tool.path()] + map(str, args)
96         run_and_throw_if_fail(bugzilla_tool_args)
97
98     def log_progress(self, patch_ids):
99         log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
100
101     def execute(self, options, args, tool):
102         self.options = options
103         self.tool = tool
104         work_queue = WorkQueue(self.name, self)
105         return work_queue.run()
106
107
108 class CommitQueue(AbstractQueue, LandingSequenceErrorHandler):
109     name = "commit-queue"
110     def __init__(self):
111         AbstractQueue.__init__(self)
112
113     # AbstractQueue methods
114
115     def begin_work_queue(self):
116         AbstractQueue.begin_work_queue(self)
117
118     def next_work_item(self):
119         patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
120         if not patches:
121             return None
122         # Only bother logging if we have patches in the queue.
123         self.log_progress([patch['id'] for patch in patches])
124         return patches[0]
125
126     def should_proceed_with_work_item(self, patch):
127         red_builders_names = self.tool.buildbot.red_core_builders_names()
128         if red_builders_names:
129             red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
130             return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
131         return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch)
132
133     def process_work_item(self, patch):
134         self._cc_watchers(patch["bug_id"])
135         self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--quiet", patch["id"]])
136
137     def handle_unexpected_error(self, patch, message):
138         self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
139
140     # LandingSequenceErrorHandler methods
141
142     @classmethod
143     def handle_script_error(cls, tool, patch, script_error):
144         tool.bugs.reject_patch_from_commit_queue(patch["id"], script_error.message_with_output())
145
146
147 class AbstractReviewQueue(AbstractQueue, PersistentPatchCollectionDelegate, LandingSequenceErrorHandler):
148     def __init__(self, options=None):
149         AbstractQueue.__init__(self, options)
150
151     # PersistentPatchCollectionDelegate methods
152
153     def collection_name(self):
154         return self.name
155
156     def fetch_potential_patch_ids(self):
157         return self.tool.bugs.fetch_attachment_ids_from_review_queue()
158
159     def status_server(self):
160         return self.tool.status()
161
162     # AbstractQueue methods
163
164     def begin_work_queue(self):
165         AbstractQueue.begin_work_queue(self)
166         self.tool.status().set_host(self.options.status_host)
167         self._patches = PersistentPatchCollection(self)
168
169     def next_work_item(self):
170         patch_id = self._patches.next()
171         if patch_id:
172             return self.tool.bugs.fetch_attachment(patch_id)
173
174     def should_proceed_with_work_item(self, patch):
175         raise NotImplementedError, "subclasses must implement"
176
177     def process_work_item(self, patch):
178         raise NotImplementedError, "subclasses must implement"
179
180     def handle_unexpected_error(self, patch, message):
181         log(message)
182
183     # LandingSequenceErrorHandler methods
184
185     @classmethod
186     def handle_script_error(cls, tool, patch, script_error):
187         log(script_error.message_with_output())
188
189
190 class StyleQueue(AbstractReviewQueue):
191     name = "style-queue"
192     def __init__(self):
193         AbstractReviewQueue.__init__(self)
194
195     def should_proceed_with_work_item(self, patch):
196         return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
197
198     def process_work_item(self, patch):
199         try:
200             self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
201             message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
202             self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
203             self._patches.did_pass(patch)
204         except ScriptError, e:
205             self._patches.did_fail(patch)
206             raise e
207
208     @classmethod
209     def handle_script_error(cls, tool, patch, script_error):
210         command = script_error.script_args
211         if type(command) is list:
212             command = command[0]
213         # FIXME: We shouldn't need to use a regexp here.  ScriptError should
214         #        have a better API.
215         if re.search("check-webkit-style", command):
216             message = "Attachment %s did not pass %s:\n\n%s" % (patch["id"], cls.name, script_error.message_with_output(output_limit=5*1024))
217             tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=cls.watchers)