2009-12-02 Eric Seidel <eric@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.webkitport import WebKitPort
45 from modules.workqueue import WorkQueue, WorkQueueDelegate
46
47 class AbstractQueue(Command, WorkQueueDelegate):
48     watchers = "webkit-bot-watchers@googlegroups.com"
49     def __init__(self, options=[]):
50         options += [
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)
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()] + 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(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         work_queue.run()
106
107
108 class CommitQueue(AbstractQueue, LandingSequenceErrorHandler):
109     name = "commit-queue"
110     show_in_main_help = False
111     def __init__(self):
112         AbstractQueue.__init__(self)
113
114     # AbstractQueue methods
115
116     def begin_work_queue(self):
117         AbstractQueue.begin_work_queue(self)
118
119     def next_work_item(self):
120         patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
121         if not patches:
122             return None
123         # Only bother logging if we have patches in the queue.
124         self.log_progress([patch['id'] for patch in patches])
125         return patches[0]
126
127     def should_proceed_with_work_item(self, patch):
128         red_builders_names = self.tool.buildbot.red_core_builders_names()
129         if red_builders_names:
130             red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
131             return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
132         return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch)
133
134     def process_work_item(self, patch):
135         self._cc_watchers(patch["bug_id"])
136         self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--quiet", patch["id"]])
137
138     def handle_unexpected_error(self, patch, message):
139         self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
140
141     # LandingSequenceErrorHandler methods
142
143     @classmethod
144     def handle_script_error(cls, tool, patch, script_error):
145         tool.bugs.reject_patch_from_commit_queue(patch["id"], script_error.message_with_output())
146
147
148 class AbstractTryQueue(AbstractQueue, PersistentPatchCollectionDelegate, LandingSequenceErrorHandler):
149     def __init__(self, options=[]):
150         AbstractQueue.__init__(self, options)
151
152     # PersistentPatchCollectionDelegate methods
153
154     def collection_name(self):
155         return self.name
156
157     def fetch_potential_patch_ids(self):
158         return self.tool.bugs.fetch_attachment_ids_from_review_queue()
159
160     def status_server(self):
161         return self.tool.status()
162
163     # AbstractQueue methods
164
165     def begin_work_queue(self):
166         AbstractQueue.begin_work_queue(self)
167         self.tool.status().set_host(self.options.status_host)
168         self._patches = PersistentPatchCollection(self)
169
170     def next_work_item(self):
171         patch_id = self._patches.next()
172         if patch_id:
173             return self.tool.bugs.fetch_attachment(patch_id)
174
175     def should_proceed_with_work_item(self, patch):
176         raise NotImplementedError, "subclasses must implement"
177
178     def process_work_item(self, patch):
179         raise NotImplementedError, "subclasses must implement"
180
181     def handle_unexpected_error(self, patch, message):
182         log(message)
183
184     # LandingSequenceErrorHandler methods
185
186     @classmethod
187     def handle_script_error(cls, tool, patch, script_error):
188         log(script_error.message_with_output())
189
190
191 class StyleQueue(AbstractTryQueue):
192     name = "style-queue"
193     show_in_main_help = False
194     def __init__(self):
195         AbstractTryQueue.__init__(self)
196
197     def should_proceed_with_work_item(self, patch):
198         return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
199
200     def process_work_item(self, patch):
201         try:
202             self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
203             message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
204             self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
205             self._patches.did_pass(patch)
206         except ScriptError, e:
207             self._patches.did_fail(patch)
208             raise e
209
210     @classmethod
211     def handle_script_error(cls, tool, patch, script_error):
212         command = script_error.script_args
213         if type(command) is list:
214             command = command[0]
215         # FIXME: We shouldn't need to use a regexp here.  ScriptError should
216         #        have a better API.
217         if re.search("check-webkit-style", command):
218             message = "Attachment %s did not pass %s:\n\n%s" % (patch["id"], cls.name, script_error.message_with_output(output_limit=5*1024))
219             tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=cls.watchers)
220
221
222 class BuildQueue(AbstractTryQueue):
223     name = "build-queue"
224     show_in_main_help = False
225     def __init__(self):
226         options = WebKitPort.port_options()
227         AbstractTryQueue.__init__(self, options)
228
229     def begin_work_queue(self):
230         AbstractTryQueue.begin_work_queue(self)
231         self.port = WebKitPort.port(self.options)
232
233     def should_proceed_with_work_item(self, patch):
234         try:
235             self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
236         except ScriptError, e:
237             return (False, "Unable to perform a build.", None)
238         return (True, "Building patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
239
240     def process_work_item(self, patch):
241         self.run_bugzilla_tool(["build-attachment", self.port.flag(), "--force-clean", "--quiet", "--non-interactive", "--parent-command=build-queue", "--no-update", patch["id"]])
242         self._patches.did_pass(patch)