2009-11-28 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 # FIXME: Trim down this import list once we have unit tests.
32 import os
33 import re
34 import StringIO
35 import subprocess
36 import sys
37 import time
38
39 from datetime import datetime, timedelta
40 from optparse import make_option
41
42 from modules.bugzilla import Bugzilla, parse_bug_id
43 from modules.buildbot import BuildBot
44 from modules.changelogs import ChangeLog
45 from modules.comments import bug_comment_from_commit_text
46 from modules.grammar import pluralize
47 from modules.landingsequence import LandingSequence, ConditionalLandingSequence, LandingSequenceErrorHandler
48 from modules.logging import error, log, tee
49 from modules.multicommandtool import MultiCommandTool, Command
50 from modules.patchcollection import PatchCollection, PersistentPatchCollection, PersistentPatchCollectionDelegate
51 from modules.processutils import run_and_throw_if_fail
52 from modules.scm import CommitMessage, detect_scm_system, ScriptError, CheckoutNeedsUpdate
53 from modules.statusbot import StatusBot
54 from modules.webkitport import WebKitPort
55 from modules.workqueue import WorkQueue, WorkQueueDelegate
56
57 class AbstractQueue(Command, WorkQueueDelegate):
58     def __init__(self, options=[]):
59         options += [
60             make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
61             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."),
62         ]
63         Command.__init__(self, "Run the %s" % self.name, options=options)
64
65     def queue_log_path(self):
66         return "%s.log" % self.name
67
68     def work_logs_directory(self):
69         return "%s-logs" % self.name
70
71     def status_host(self):
72         return self.options.status_host
73
74     def begin_work_queue(self):
75         log("CAUTION: %s will discard all local changes in %s" % (self.name, self.tool.scm().checkout_root))
76         if self.options.confirm:
77             response = raw_input("Are you sure?  Type \"yes\" to continue: ")
78             if (response != "yes"):
79                 error("User declined.")
80         log("Running WebKit %s. %s" % (self.name, datetime.now().strftime(WorkQueue.log_date_format)))
81
82     def should_continue_work_queue(self):
83         return True
84
85     def next_work_item(self):
86         raise NotImplementedError, "subclasses must implement"
87
88     def should_proceed_with_work_item(self, work_item):
89         raise NotImplementedError, "subclasses must implement"
90
91     def process_work_item(self, work_item):
92         raise NotImplementedError, "subclasses must implement"
93
94     def handle_unexpected_error(self, work_item, message):
95         raise NotImplementedError, "subclasses must implement"
96
97     def run_bugzilla_tool(self, args):
98         bugzilla_tool_args = [self.tool.path()] + args
99         run_and_throw_if_fail(bugzilla_tool_args)
100
101     def log_progress(self, patch_ids):
102         log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(patch_ids)))
103
104     def execute(self, options, args, tool):
105         self.options = options
106         self.tool = tool
107         work_queue = WorkQueue(self.name, self)
108         work_queue.run()
109
110
111 class CommitQueue(AbstractQueue, LandingSequenceErrorHandler):
112     name = "commit-queue"
113     show_in_main_help = False
114     def __init__(self):
115         AbstractQueue.__init__(self)
116
117     # AbstractQueue methods
118
119     def begin_work_queue(self):
120         AbstractQueue.begin_work_queue(self)
121
122     def next_work_item(self):
123         patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
124         if not patches:
125             return None
126         # Only bother logging if we have patches in the queue.
127         self.log_progress([patch['id'] for patch in patches])
128         return patches[0]
129
130     def should_proceed_with_work_item(self, patch):
131         red_builders_names = self.tool.buildbot.red_core_builders_names()
132         if red_builders_names:
133             red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
134             return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
135         return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch)
136
137     def process_work_item(self, patch):
138         self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--quiet", patch["id"]])
139
140     def handle_unexpected_error(self, patch, message):
141         self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
142
143     # LandingSequenceErrorHandler methods
144
145     @classmethod
146     def handle_script_error(cls, tool, patch, script_error):
147         tool.bugs.reject_patch_from_commit_queue(patch["id"], script_error.message_with_output())
148
149
150 class AbstractTryQueue(AbstractQueue, PersistentPatchCollectionDelegate, LandingSequenceErrorHandler):
151     def __init__(self, options=[]):
152         AbstractQueue.__init__(self, options)
153
154     # PersistentPatchCollectionDelegate methods
155
156     def collection_name(self):
157         return self.name
158
159     def fetch_potential_patches(self):
160         return self.tool.bugs.fetch_patches_from_review_queue(limit=3)
161
162     def status_server(self):
163         return self.tool.status()
164
165     # AbstractQueue methods
166
167     def begin_work_queue(self):
168         AbstractQueue.begin_work_queue(self)
169         self.tool.status().set_host(self.options.status_host)
170         self._patches = PersistentPatchCollection(self)
171
172     def next_work_item(self):
173         return self._patches.next()
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         self._patches.done(patch)
184
185     # LandingSequenceErrorHandler methods
186
187     @classmethod
188     def handle_script_error(cls, tool, patch, script_error):
189         log(script_error.message_with_output())
190
191
192 class StyleQueue(AbstractTryQueue):
193     name = "style-queue"
194     show_in_main_help = False
195     def __init__(self):
196         AbstractTryQueue.__init__(self)
197
198     def should_proceed_with_work_item(self, patch):
199         return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
200
201     def process_work_item(self, patch):
202         self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
203         self._patches.done(patch)
204
205     @classmethod
206     def handle_script_error(cls, tool, patch, script_error):
207         command = script_error.script_args
208         if type(command) is list:
209             command = command[0]
210         # FIXME: We shouldn't need to use a regexp here.  ScriptError should
211         #        have a better API.
212         if re.search("check-webkit-style", command):
213             message = "Attachment %s did not pass %s:\n\n%s" % (patch["id"], cls.name, script_error.message_with_output(output_limit=None))
214             # Local-only logging helpful for development:
215             # log("** BEGIN BUG POST **\n%s** END BUG POST **" % message)
216             tool.bugs.post_comment_to_bug(patch["bug_id"], message)
217
218
219 class BuildQueue(AbstractTryQueue):
220     name = "build-queue"
221     show_in_main_help = False
222     def __init__(self):
223         options = WebKitPort.port_options()
224         AbstractTryQueue.__init__(self, options)
225
226     def begin_work_queue(self):
227         AbstractTryQueue.begin_work_queue(self)
228         self.port = WebKitPort.port(self.options)
229
230     def should_proceed_with_work_item(self, patch):
231         try:
232             self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
233         except ScriptError, e:
234             return (False, "Unable to perform a build.", None)
235         return (True, "Building patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
236
237     def process_work_item(self, patch):
238         self.run_bugzilla_tool(["build-attachment", self.port.flag(), "--force-clean", "--quiet", "--non-interactive", "--parent-command=build-queue", "--no-update", patch["id"]])
239         self._patches.done(patch)