df56ef6e77e486fe96ad4e33f8419c07b3f78788
[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     watchers = "webkit-bot-watchers@googlegroups.com"
59     def __init__(self, options=[]):
60         options += [
61             make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
62             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."),
63         ]
64         Command.__init__(self, "Run the %s" % self.name, options=options)
65
66     def _cc_watchers(self, bug_id):
67         try:
68             self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
69         except Exception, e:
70             log("Failed to CC watchers: %s." % e)
71
72     def queue_log_path(self):
73         return "%s.log" % self.name
74
75     def work_logs_directory(self):
76         return "%s-logs" % self.name
77
78     def status_host(self):
79         return self.options.status_host
80
81     def begin_work_queue(self):
82         log("CAUTION: %s will discard all local changes in %s" % (self.name, self.tool.scm().checkout_root))
83         if self.options.confirm:
84             response = raw_input("Are you sure?  Type \"yes\" to continue: ")
85             if (response != "yes"):
86                 error("User declined.")
87         log("Running WebKit %s. %s" % (self.name, datetime.now().strftime(WorkQueue.log_date_format)))
88
89     def should_continue_work_queue(self):
90         return True
91
92     def next_work_item(self):
93         raise NotImplementedError, "subclasses must implement"
94
95     def should_proceed_with_work_item(self, work_item):
96         raise NotImplementedError, "subclasses must implement"
97
98     def process_work_item(self, work_item):
99         raise NotImplementedError, "subclasses must implement"
100
101     def handle_unexpected_error(self, work_item, message):
102         raise NotImplementedError, "subclasses must implement"
103
104     def run_bugzilla_tool(self, args):
105         bugzilla_tool_args = [self.tool.path()] + args
106         run_and_throw_if_fail(bugzilla_tool_args)
107
108     def log_progress(self, patch_ids):
109         log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(patch_ids)))
110
111     def execute(self, options, args, tool):
112         self.options = options
113         self.tool = tool
114         work_queue = WorkQueue(self.name, self)
115         work_queue.run()
116
117
118 class CommitQueue(AbstractQueue, LandingSequenceErrorHandler):
119     name = "commit-queue"
120     show_in_main_help = False
121     def __init__(self):
122         AbstractQueue.__init__(self)
123
124     # AbstractQueue methods
125
126     def begin_work_queue(self):
127         AbstractQueue.begin_work_queue(self)
128
129     def next_work_item(self):
130         patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
131         if not patches:
132             return None
133         # Only bother logging if we have patches in the queue.
134         self.log_progress([patch['id'] for patch in patches])
135         return patches[0]
136
137     def should_proceed_with_work_item(self, patch):
138         red_builders_names = self.tool.buildbot.red_core_builders_names()
139         if red_builders_names:
140             red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
141             return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
142         return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch)
143
144     def process_work_item(self, patch):
145         self._cc_watchers(patch["bug_id"])
146         self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--quiet", patch["id"]])
147
148     def handle_unexpected_error(self, patch, message):
149         self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
150
151     # LandingSequenceErrorHandler methods
152
153     @classmethod
154     def handle_script_error(cls, tool, patch, script_error):
155         tool.bugs.reject_patch_from_commit_queue(patch["id"], script_error.message_with_output())
156
157
158 class AbstractTryQueue(AbstractQueue, PersistentPatchCollectionDelegate, LandingSequenceErrorHandler):
159     def __init__(self, options=[]):
160         AbstractQueue.__init__(self, options)
161
162     # PersistentPatchCollectionDelegate methods
163
164     def collection_name(self):
165         return self.name
166
167     def fetch_potential_patch_ids(self):
168         return self.tool.bugs.fetch_attachment_ids_from_review_queue()
169
170     def status_server(self):
171         return self.tool.status()
172
173     # AbstractQueue methods
174
175     def begin_work_queue(self):
176         AbstractQueue.begin_work_queue(self)
177         self.tool.status().set_host(self.options.status_host)
178         self._patches = PersistentPatchCollection(self)
179
180     def next_work_item(self):
181         patch_id = self._patches.next()
182         if patch_id:
183             return self.tool.bugs.fetch_attachment(patch_id)
184
185     def should_proceed_with_work_item(self, patch):
186         raise NotImplementedError, "subclasses must implement"
187
188     def process_work_item(self, patch):
189         raise NotImplementedError, "subclasses must implement"
190
191     def handle_unexpected_error(self, patch, message):
192         log(message)
193
194     # LandingSequenceErrorHandler methods
195
196     @classmethod
197     def handle_script_error(cls, tool, patch, script_error):
198         log(script_error.message_with_output())
199
200
201 class StyleQueue(AbstractTryQueue):
202     name = "style-queue"
203     show_in_main_help = False
204     def __init__(self):
205         AbstractTryQueue.__init__(self)
206
207     def should_proceed_with_work_item(self, patch):
208         return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
209
210     def process_work_item(self, patch):
211         try:
212             self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
213             message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
214             self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
215             self._patches.did_pass(patch)
216         except ScriptError, e:
217             self._patches.did_fail(patch)
218             raise e
219
220     @classmethod
221     def handle_script_error(cls, tool, patch, script_error):
222         command = script_error.script_args
223         if type(command) is list:
224             command = command[0]
225         # FIXME: We shouldn't need to use a regexp here.  ScriptError should
226         #        have a better API.
227         if re.search("check-webkit-style", command):
228             message = "Attachment %s did not pass %s:\n\n%s" % (patch["id"], cls.name, script_error.message_with_output(output_limit=5*1024))
229             tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=cls.watchers)
230
231
232 class BuildQueue(AbstractTryQueue):
233     name = "build-queue"
234     show_in_main_help = False
235     def __init__(self):
236         options = WebKitPort.port_options()
237         AbstractTryQueue.__init__(self, options)
238
239     def begin_work_queue(self):
240         AbstractTryQueue.begin_work_queue(self)
241         self.port = WebKitPort.port(self.options)
242
243     def should_proceed_with_work_item(self, patch):
244         try:
245             self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
246         except ScriptError, e:
247             return (False, "Unable to perform a build.", None)
248         return (True, "Building patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
249
250     def process_work_item(self, patch):
251         self.run_bugzilla_tool(["build-attachment", self.port.flag(), "--force-clean", "--quiet", "--non-interactive", "--parent-command=build-queue", "--no-update", patch["id"]])
252         self._patches.did_pass(patch)