2009-11-20 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
48 from modules.logging import error, log, tee
49 from modules.multicommandtool import MultiCommandTool, Command
50 from modules.patchcollection import PatchCollection
51 from modules.scm import CommitMessage, detect_scm_system, ScriptError, CheckoutNeedsUpdate
52 from modules.statusbot import StatusBot
53 from modules.webkitlandingscripts import WebKitLandingScripts, commit_message_for_this_commit
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     @staticmethod
98     def run_bugzilla_tool(args):
99         bugzilla_tool_path = __file__ # re-execute this script
100         bugzilla_tool_args = [bugzilla_tool_path] + args
101         WebKitLandingScripts.run_and_throw_if_fail(bugzilla_tool_args)
102
103     def log_progress(self, patch_ids):
104         log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(patch_ids)))
105
106     def execute(self, options, args, tool):
107         self.options = options
108         self.tool = tool
109         work_queue = WorkQueue(self)
110         work_queue.run()
111
112
113 class CommitQueue(AbstractQueue):
114     name = "commit-queue"
115     def __init__(self):
116         AbstractQueue.__init__(self)
117
118     def begin_work_queue(self):
119         AbstractQueue.begin_work_queue(self)
120
121     def next_work_item(self):
122         patches = self.tool.bugs.fetch_patches_from_commit_queue(reject_invalid_patches=True)
123         if not patches:
124             return None
125         # Only bother logging if we have patches in the queue.
126         self.log_progress([patch['id'] for patch in patches])
127         return patches[0]
128
129     def should_proceed_with_work_item(self, patch):
130         red_builders_names = self.tool.buildbot.red_core_builders_names()
131         if red_builders_names:
132             red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
133             return (False, "Builders [%s] are red. See http://build.webkit.org." % ", ".join(red_builders_names), None)
134         return (True, "Landing patch %s from bug %s." % (patch["id"], patch["bug_id"]), patch["bug_id"])
135
136     def process_work_item(self, patch):
137         self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--quiet", patch["id"]])
138
139     def handle_unexpected_error(self, patch, message):
140         self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
141
142
143 class AbstractTryQueue(AbstractQueue):
144     def __init__(self, options=[]):
145         AbstractQueue.__init__(self, options)
146
147     def status_host(self):
148         return None # FIXME: A hack until we come up with a more generic status page.
149
150     def begin_work_queue(self):
151         AbstractQueue.begin_work_queue(self)
152         self._patches = PatchCollection(self.tool.bugs)
153         self._patches.add_patches(self.tool.bugs.fetch_patches_from_review_queue(limit=10))
154
155     def next_work_item(self):
156         self.log_progress(self._patches.patch_ids())
157         return self._patches.next()
158
159     def should_proceed_with_work_item(self, patch):
160         raise NotImplementedError, "subclasses must implement"
161
162     def process_work_item(self, patch):
163         raise NotImplementedError, "subclasses must implement"
164
165     def handle_unexpected_error(self, patch, message):
166         log(message)
167
168
169 class StyleQueue(AbstractTryQueue):
170     name = "style-queue"
171     def __init__(self):
172         AbstractTryQueue.__init__(self)
173
174     def should_proceed_with_work_item(self, patch):
175         return (True, "Checking style for patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch["bug_id"])
176
177     def process_work_item(self, patch):
178         self.run_bugzilla_tool(["check-style", "--force-clean", patch["id"]])
179
180
181 class BuildQueue(AbstractTryQueue):
182     name = "build-queue"
183     def __init__(self):
184         options = WebKitPort.port_options()
185         AbstractTryQueue.__init__(self, options)
186
187     def begin_work_queue(self):
188         AbstractTryQueue.begin_work_queue(self)
189         self.port = WebKitPort.port(self.options)
190
191     def should_proceed_with_work_item(self, patch):
192         try:
193             self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
194         except ScriptError, e:
195             return (False, "Unable to perform a build.", None)
196         return (True, "Building patch %s on bug %s." % (patch["id"], patch["bug_id"]), patch["bug_id"])
197
198     def process_work_item(self, patch):
199         self.run_bugzilla_tool(["build-attachment", self.port.flag(), "--force-clean", "--quiet", "--no-update", patch["id"]])