Clean up ChunkedUpdateDrawingAreaProxy
[WebKit-https.git] / Tools / Scripts / webkitpy / tool / commands / queues.py
1 # Copyright (c) 2009 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 from __future__ import with_statement
31
32 import codecs
33 import time
34 import traceback
35 import os
36
37 from datetime import datetime
38 from optparse import make_option
39 from StringIO import StringIO
40
41 from webkitpy.common.config.committervalidator import CommitterValidator
42 from webkitpy.common.net.bugzilla import Attachment
43 from webkitpy.common.net.layouttestresults import LayoutTestResults
44 from webkitpy.common.net.statusserver import StatusServer
45 from webkitpy.common.system.deprecated_logging import error, log
46 from webkitpy.common.system.executive import ScriptError
47 from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate
48 from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder
49 from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
50 from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter
51 from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
52 from webkitpy.tool.multicommandtool import Command, TryAgain
53
54
55 class AbstractQueue(Command, QueueEngineDelegate):
56     watchers = [
57     ]
58
59     _pass_status = "Pass"
60     _fail_status = "Fail"
61     _retry_status = "Retry"
62     _error_status = "Error"
63
64     def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
65         options_list = (options or []) + [
66             make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
67             make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."),
68         ]
69         Command.__init__(self, "Run the %s" % self.name, options=options_list)
70         self._iteration_count = 0
71
72     def _cc_watchers(self, bug_id):
73         try:
74             self._tool.bugs.add_cc_to_bug(bug_id, self.watchers)
75         except Exception, e:
76             traceback.print_exc()
77             log("Failed to CC watchers.")
78
79     def run_webkit_patch(self, args):
80         webkit_patch_args = [self._tool.path()]
81         # FIXME: This is a hack, we should have a more general way to pass global options.
82         # FIXME: We must always pass global options and their value in one argument
83         # because our global option code looks for the first argument which does
84         # not begin with "-" and assumes that is the command name.
85         webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host]
86         if self._tool.status_server.bot_id:
87             webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id]
88         if self._options.port:
89             webkit_patch_args += ["--port=%s" % self._options.port]
90         webkit_patch_args.extend(args)
91         # FIXME: There is probably no reason to use run_and_throw_if_fail anymore.
92         # run_and_throw_if_fail was invented to support tee'd output
93         # (where we write both to a log file and to the console at once),
94         # but the queues don't need live-progress, a dump-of-output at the
95         # end should be sufficient.
96         return self._tool.executive.run_and_throw_if_fail(webkit_patch_args)
97
98     def _log_directory(self):
99         return "%s-logs" % self.name
100
101     # QueueEngineDelegate methods
102
103     def queue_log_path(self):
104         return os.path.join(self._log_directory(), "%s.log" % self.name)
105
106     def work_item_log_path(self, work_item):
107         raise NotImplementedError, "subclasses must implement"
108
109     def begin_work_queue(self):
110         log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root))
111         if self._options.confirm:
112             response = self._tool.user.prompt("Are you sure?  Type \"yes\" to continue: ")
113             if (response != "yes"):
114                 error("User declined.")
115         log("Running WebKit %s." % self.name)
116         self._tool.status_server.update_status(self.name, "Starting Queue")
117
118     def stop_work_queue(self, reason):
119         self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason)
120
121     def should_continue_work_queue(self):
122         self._iteration_count += 1
123         return not self._options.iterations or self._iteration_count <= self._options.iterations
124
125     def next_work_item(self):
126         raise NotImplementedError, "subclasses must implement"
127
128     def should_proceed_with_work_item(self, work_item):
129         raise NotImplementedError, "subclasses must implement"
130
131     def process_work_item(self, work_item):
132         raise NotImplementedError, "subclasses must implement"
133
134     def handle_unexpected_error(self, work_item, message):
135         raise NotImplementedError, "subclasses must implement"
136
137     # Command methods
138
139     def execute(self, options, args, tool, engine=QueueEngine):
140         self._options = options # FIXME: This code is wrong.  Command.options is a list, this assumes an Options element!
141         self._tool = tool  # FIXME: This code is wrong too!  Command.bind_to_tool handles this!
142         return engine(self.name, self, self._tool.wakeup_event).run()
143
144     @classmethod
145     def _log_from_script_error_for_upload(cls, script_error, output_limit=None):
146         # We have seen request timeouts with app engine due to large
147         # log uploads.  Trying only the last 512k.
148         if not output_limit:
149             output_limit = 512 * 1024  # 512k
150         output = script_error.message_with_output(output_limit=output_limit)
151         # We pre-encode the string to a byte array before passing it
152         # to status_server, because ClientForm (part of mechanize)
153         # wants a file-like object with pre-encoded data.
154         return StringIO(output.encode("utf-8"))
155
156     @classmethod
157     def _update_status_for_script_error(cls, tool, state, script_error, is_error=False):
158         message = str(script_error)
159         if is_error:
160             message = "Error: %s" % message
161         failure_log = cls._log_from_script_error_for_upload(script_error)
162         return tool.status_server.update_status(cls.name, message, state["patch"], failure_log)
163
164
165 class FeederQueue(AbstractQueue):
166     name = "feeder-queue"
167
168     _sleep_duration = 30  # seconds
169
170     # AbstractPatchQueue methods
171
172     def begin_work_queue(self):
173         AbstractQueue.begin_work_queue(self)
174         self.feeders = [
175             CommitQueueFeeder(self._tool),
176             EWSFeeder(self._tool),
177         ]
178
179     def next_work_item(self):
180         # This really show inherit from some more basic class that doesn't
181         # understand work items, but the base class in the heirarchy currently
182         # understands work items.
183         return "synthetic-work-item"
184
185     def should_proceed_with_work_item(self, work_item):
186         return True
187
188     def process_work_item(self, work_item):
189         for feeder in self.feeders:
190             feeder.feed()
191         time.sleep(self._sleep_duration)
192         return True
193
194     def work_item_log_path(self, work_item):
195         return None
196
197     def handle_unexpected_error(self, work_item, message):
198         log(message)
199
200
201 class AbstractPatchQueue(AbstractQueue):
202     def _update_status(self, message, patch=None, results_file=None):
203         return self._tool.status_server.update_status(self.name, message, patch, results_file)
204
205     def _next_patch(self):
206         patch_id = self._tool.status_server.next_work_item(self.name)
207         if not patch_id:
208             return None
209         patch = self._tool.bugs.fetch_attachment(patch_id)
210         if not patch:
211             # FIXME: Using a fake patch because release_work_item has the wrong API.
212             # We also don't really need to release the lock (although that's fine),
213             # mostly we just need to remove this bogus patch from our queue.
214             # If for some reason bugzilla is just down, then it will be re-fed later.
215             patch = Attachment({'id': patch_id}, None)
216             self._release_work_item(patch)
217             return None
218         return patch
219
220     def _release_work_item(self, patch):
221         self._tool.status_server.release_work_item(self.name, patch)
222
223     def _did_pass(self, patch):
224         self._update_status(self._pass_status, patch)
225         self._release_work_item(patch)
226
227     def _did_fail(self, patch):
228         self._update_status(self._fail_status, patch)
229         self._release_work_item(patch)
230
231     def _did_retry(self, patch):
232         self._update_status(self._retry_status, patch)
233         self._release_work_item(patch)
234
235     def _did_error(self, patch, reason):
236         message = "%s: %s" % (self._error_status, reason)
237         self._update_status(message, patch)
238         self._release_work_item(patch)
239
240     def work_item_log_path(self, patch):
241         return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
242
243
244 class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate):
245     name = "commit-queue"
246
247     # AbstractPatchQueue methods
248
249     def begin_work_queue(self):
250         AbstractPatchQueue.begin_work_queue(self)
251         self.committer_validator = CommitterValidator(self._tool.bugs)
252
253     def next_work_item(self):
254         return self._next_patch()
255
256     def should_proceed_with_work_item(self, patch):
257         patch_text = "rollout patch" if patch.is_rollout() else "patch"
258         self._update_status("Processing %s" % patch_text, patch)
259         return True
260
261     def process_work_item(self, patch):
262         self._cc_watchers(patch.bug_id())
263         task = CommitQueueTask(self, patch)
264         try:
265             if task.run():
266                 self._did_pass(patch)
267                 return True
268             self._did_retry(patch)
269         except ScriptError, e:
270             validator = CommitterValidator(self._tool.bugs)
271             validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e))
272             self._did_fail(patch)
273
274     def _error_message_for_bug(self, status_id, script_error):
275         if not script_error.output:
276             return script_error.message_with_output()
277         results_link = self._tool.status_server.results_url_for_status(status_id)
278         return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
279
280     def handle_unexpected_error(self, patch, message):
281         self.committer_validator.reject_patch_from_commit_queue(patch.id(), message)
282
283     # CommitQueueTaskDelegate methods
284
285     def run_command(self, command):
286         self.run_webkit_patch(command)
287
288     def command_passed(self, message, patch):
289         self._update_status(message, patch=patch)
290
291     def command_failed(self, message, script_error, patch):
292         failure_log = self._log_from_script_error_for_upload(script_error)
293         return self._update_status(message, patch=patch, results_file=failure_log)
294
295     # FIXME: This exists for mocking, but should instead be mocked via
296     # tool.filesystem.read_text_file.  They have different error handling at the moment.
297     def _read_file_contents(self, path):
298         try:
299             with codecs.open(path, "r", "utf-8") as open_file:
300                 return open_file.read()
301         except OSError, e:  # File does not exist or can't be read.
302             return None
303
304     # FIXME: This may belong on the Port object.
305     def layout_test_results(self):
306         results_path = self._tool.port().layout_tests_results_path()
307         results_html = self._read_file_contents(results_path)
308         if not results_html:
309             return None
310         return LayoutTestResults.results_from_string(results_html)
311
312     def refetch_patch(self, patch):
313         return self._tool.bugs.fetch_attachment(patch.id())
314
315     def report_flaky_tests(self, patch, flaky_tests):
316         reporter = FlakyTestReporter(self._tool, self.name)
317         reporter.report_flaky_tests(flaky_tests, patch)
318
319     # StepSequenceErrorHandler methods
320
321     def handle_script_error(cls, tool, state, script_error):
322         # Hitting this error handler should be pretty rare.  It does occur,
323         # however, when a patch no longer applies to top-of-tree in the final
324         # land step.
325         log(script_error.message_with_output())
326
327     @classmethod
328     def handle_checkout_needs_update(cls, tool, state, options, error):
329         message = "Tests passed, but commit failed (checkout out of date).  Updating, then landing without building or re-running tests."
330         tool.status_server.update_status(cls.name, message, state["patch"])
331         # The only time when we find out that out checkout needs update is
332         # when we were ready to actually pull the trigger and land the patch.
333         # Rather than spinning in the master process, we retry without
334         # building or testing, which is much faster.
335         options.build = False
336         options.test = False
337         options.update = True
338         raise TryAgain()
339
340
341 class AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler):
342     """This is the base-class for the EWS queues and the style-queue."""
343     def __init__(self, options=None):
344         AbstractPatchQueue.__init__(self, options)
345
346     def review_patch(self, patch):
347         raise NotImplementedError("subclasses must implement")
348
349     # AbstractPatchQueue methods
350
351     def begin_work_queue(self):
352         AbstractPatchQueue.begin_work_queue(self)
353
354     def next_work_item(self):
355         return self._next_patch()
356
357     def should_proceed_with_work_item(self, patch):
358         raise NotImplementedError("subclasses must implement")
359
360     def process_work_item(self, patch):
361         try:
362             if not self.review_patch(patch):
363                 return False
364             self._did_pass(patch)
365             return True
366         except ScriptError, e:
367             if e.exit_code != QueueEngine.handled_error_code:
368                 self._did_fail(patch)
369             else:
370                 # The subprocess handled the error, but won't have released the patch, so we do.
371                 # FIXME: We need to simplify the rules by which _release_work_item is called.
372                 self._release_work_item(patch)
373             raise e
374
375     def handle_unexpected_error(self, patch, message):
376         log(message)
377
378     # StepSequenceErrorHandler methods
379
380     @classmethod
381     def handle_script_error(cls, tool, state, script_error):
382         log(script_error.message_with_output())
383
384
385 class StyleQueue(AbstractReviewQueue):
386     name = "style-queue"
387     def __init__(self):
388         AbstractReviewQueue.__init__(self)
389
390     def should_proceed_with_work_item(self, patch):
391         self._update_status("Checking style", patch)
392         return True
393
394     def review_patch(self, patch):
395         self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()])
396         return True
397
398     @classmethod
399     def handle_script_error(cls, tool, state, script_error):
400         is_svn_apply = script_error.command_name() == "svn-apply"
401         status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
402         if is_svn_apply:
403             QueueEngine.exit_after_handled_error(script_error)
404         message = "Attachment %s did not pass %s:\n\n%s\n\nIf any of these errors are false positives, please file a bug against check-webkit-style." % (state["patch"].id(), cls.name, script_error.message_with_output(output_limit=3*1024))
405         tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers)
406         exit(1)