918397fc573e2da6ca966c886b571558fb3a0c9f
[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
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):
112     name = "commit-queue"
113     show_in_main_help = False
114     def __init__(self):
115         AbstractQueue.__init__(self)
116
117     def begin_work_queue(self):
118         AbstractQueue.begin_work_queue(self)
119
120     def next_work_item(self):
121         patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
122         if not patches:
123             return None
124         # Only bother logging if we have patches in the queue.
125         self.log_progress([patch['id'] for patch in patches])
126         return patches[0]
127
128     def should_proceed_with_work_item(self, patch):
129         red_builders_names = self.tool.buildbot.red_core_builders_names()
130         if red_builders_names:
131             red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
132             return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
133         return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch)
134
135     def process_work_item(self, patch):
136         self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--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
142 class AbstractTryQueue(AbstractQueue, PersistentPatchCollectionDelegate):
143     def __init__(self, options=[]):
144         AbstractQueue.__init__(self, options)
145
146     # PersistentPatchCollectionDelegate methods
147
148     def collection_name(self):
149         return self.name
150
151     def fetch_potential_patches(self):
152         return self.tool.bugs.fetch_patches_from_review_queue(limit=3)
153
154     def status_server(self):
155         return self.tool.status()
156
157     # AbstractQueue methods
158
159     def begin_work_queue(self):
160         AbstractQueue.begin_work_queue(self)
161         self.tool.status().set_host(self.options.status_host)
162         self._patches = PersistentPatchCollection(self)
163
164     def next_work_item(self):
165         return self._patches.next()
166
167     def should_proceed_with_work_item(self, patch):
168         raise NotImplementedError, "subclasses must implement"
169
170     def process_work_item(self, patch):
171         raise NotImplementedError, "subclasses must implement"
172
173     def handle_unexpected_error(self, patch, message):
174         log(message)
175         self._patches.done(patch)
176
177
178 class StyleQueue(AbstractTryQueue):
179     name = "style-queue"
180     show_in_main_help = False
181     def __init__(self):
182         AbstractTryQueue.__init__(self)
183
184     def should_proceed_with_work_item(self, patch):
185         return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
186
187     def process_work_item(self, patch):
188         self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", patch["id"]])
189         self._patches.done(patch)
190
191
192 class BuildQueue(AbstractTryQueue):
193     name = "build-queue"
194     show_in_main_help = False
195     def __init__(self):
196         options = WebKitPort.port_options()
197         AbstractTryQueue.__init__(self, options)
198
199     def begin_work_queue(self):
200         AbstractTryQueue.begin_work_queue(self)
201         self.port = WebKitPort.port(self.options)
202
203     def should_proceed_with_work_item(self, patch):
204         try:
205             self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
206         except ScriptError, e:
207             return (False, "Unable to perform a build.", None)
208         return (True, "Building patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch)
209
210     def process_work_item(self, patch):
211         self.run_bugzilla_tool(["build-attachment", self.port.flag(), "--force-clean", "--quiet", "--no-update", patch["id"]])
212         self._patches.done(patch)