initial import
[vuplus_webkit] / 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.statusserver import StatusServer
44 from webkitpy.common.system.deprecated_logging import error, log
45 from webkitpy.common.system.executive import ScriptError
46 from webkitpy.tool.bot.botinfo import BotInfo
47 from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate
48 from webkitpy.tool.bot.expectedfailures import ExpectedFailures
49 from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder
50 from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader
51 from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
52 from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter
53 from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
54 from webkitpy.tool.multicommandtool import Command, TryAgain
55
56
57 class AbstractQueue(Command, QueueEngineDelegate):
58     watchers = [
59     ]
60
61     _pass_status = "Pass"
62     _fail_status = "Fail"
63     _retry_status = "Retry"
64     _error_status = "Error"
65
66     def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
67         options_list = (options or []) + [
68             make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
69             make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."),
70         ]
71         Command.__init__(self, "Run the %s" % self.name, options=options_list)
72         self._iteration_count = 0
73
74     def _cc_watchers(self, bug_id):
75         try:
76             self._tool.bugs.add_cc_to_bug(bug_id, self.watchers)
77         except Exception, e:
78             traceback.print_exc()
79             log("Failed to CC watchers.")
80
81     def run_webkit_patch(self, args):
82         webkit_patch_args = [self._tool.path()]
83         # FIXME: This is a hack, we should have a more general way to pass global options.
84         # FIXME: We must always pass global options and their value in one argument
85         # because our global option code looks for the first argument which does
86         # not begin with "-" and assumes that is the command name.
87         webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host]
88         if self._tool.status_server.bot_id:
89             webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id]
90         if self._options.port:
91             webkit_patch_args += ["--port=%s" % self._options.port]
92         webkit_patch_args.extend(args)
93         # FIXME: There is probably no reason to use run_and_throw_if_fail anymore.
94         # run_and_throw_if_fail was invented to support tee'd output
95         # (where we write both to a log file and to the console at once),
96         # but the queues don't need live-progress, a dump-of-output at the
97         # end should be sufficient.
98         return self._tool.executive.run_and_throw_if_fail(webkit_patch_args, cwd=self._tool.scm().checkout_root)
99
100     def _log_directory(self):
101         return os.path.join("..", "%s-logs" % self.name)
102
103     # QueueEngineDelegate methods
104
105     def queue_log_path(self):
106         return os.path.join(self._log_directory(), "%s.log" % self.name)
107
108     def work_item_log_path(self, work_item):
109         raise NotImplementedError, "subclasses must implement"
110
111     def begin_work_queue(self):
112         log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root))
113         if self._options.confirm:
114             response = self._tool.user.prompt("Are you sure?  Type \"yes\" to continue: ")
115             if (response != "yes"):
116                 error("User declined.")
117         log("Running WebKit %s." % self.name)
118         self._tool.status_server.update_status(self.name, "Starting Queue")
119
120     def stop_work_queue(self, reason):
121         self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason)
122
123     def should_continue_work_queue(self):
124         self._iteration_count += 1
125         return not self._options.iterations or self._iteration_count <= self._options.iterations
126
127     def next_work_item(self):
128         raise NotImplementedError, "subclasses must implement"
129
130     def should_proceed_with_work_item(self, work_item):
131         raise NotImplementedError, "subclasses must implement"
132
133     def process_work_item(self, work_item):
134         raise NotImplementedError, "subclasses must implement"
135
136     def handle_unexpected_error(self, work_item, message):
137         raise NotImplementedError, "subclasses must implement"
138
139     # Command methods
140
141     def execute(self, options, args, tool, engine=QueueEngine):
142         self._options = options # FIXME: This code is wrong.  Command.options is a list, this assumes an Options element!
143         self._tool = tool  # FIXME: This code is wrong too!  Command.bind_to_tool handles this!
144         return engine(self.name, self, self._tool.wakeup_event).run()
145
146     @classmethod
147     def _log_from_script_error_for_upload(cls, script_error, output_limit=None):
148         # We have seen request timeouts with app engine due to large
149         # log uploads.  Trying only the last 512k.
150         if not output_limit:
151             output_limit = 512 * 1024  # 512k
152         output = script_error.message_with_output(output_limit=output_limit)
153         # We pre-encode the string to a byte array before passing it
154         # to status_server, because ClientForm (part of mechanize)
155         # wants a file-like object with pre-encoded data.
156         return StringIO(output.encode("utf-8"))
157
158     @classmethod
159     def _update_status_for_script_error(cls, tool, state, script_error, is_error=False):
160         message = str(script_error)
161         if is_error:
162             message = "Error: %s" % message
163         failure_log = cls._log_from_script_error_for_upload(script_error)
164         return tool.status_server.update_status(cls.name, message, state["patch"], failure_log)
165
166
167 class FeederQueue(AbstractQueue):
168     name = "feeder-queue"
169
170     _sleep_duration = 30  # seconds
171
172     # AbstractQueue methods
173
174     def begin_work_queue(self):
175         AbstractQueue.begin_work_queue(self)
176         self.feeders = [
177             CommitQueueFeeder(self._tool),
178             EWSFeeder(self._tool),
179         ]
180
181     def next_work_item(self):
182         # This really show inherit from some more basic class that doesn't
183         # understand work items, but the base class in the heirarchy currently
184         # understands work items.
185         return "synthetic-work-item"
186
187     def should_proceed_with_work_item(self, work_item):
188         return True
189
190     def process_work_item(self, work_item):
191         for feeder in self.feeders:
192             feeder.feed()
193         time.sleep(self._sleep_duration)
194         return True
195
196     def work_item_log_path(self, work_item):
197         return None
198
199     def handle_unexpected_error(self, work_item, message):
200         log(message)
201
202
203 class AbstractPatchQueue(AbstractQueue):
204     def _update_status(self, message, patch=None, results_file=None):
205         return self._tool.status_server.update_status(self.name, message, patch, results_file)
206
207     def _next_patch(self):
208         patch_id = self._tool.status_server.next_work_item(self.name)
209         if not patch_id:
210             return None
211         patch = self._tool.bugs.fetch_attachment(patch_id)
212         if not patch:
213             # FIXME: Using a fake patch because release_work_item has the wrong API.
214             # We also don't really need to release the lock (although that's fine),
215             # mostly we just need to remove this bogus patch from our queue.
216             # If for some reason bugzilla is just down, then it will be re-fed later.
217             patch = Attachment({'id': patch_id}, None)
218             self._release_work_item(patch)
219             return None
220         return patch
221
222     def _release_work_item(self, patch):
223         self._tool.status_server.release_work_item(self.name, patch)
224
225     def _did_pass(self, patch):
226         self._update_status(self._pass_status, patch)
227         self._release_work_item(patch)
228
229     def _did_fail(self, patch):
230         self._update_status(self._fail_status, patch)
231         self._release_work_item(patch)
232
233     def _did_retry(self, patch):
234         self._update_status(self._retry_status, patch)
235         self._release_work_item(patch)
236
237     def _did_error(self, patch, reason):
238         message = "%s: %s" % (self._error_status, reason)
239         self._update_status(message, patch)
240         self._release_work_item(patch)
241
242     # FIXME: This probably belongs at a layer below AbstractPatchQueue, but shared by CommitQueue and the EarlyWarningSystem.
243     def _upload_results_archive_for_patch(self, patch, results_archive_zip):
244         bot_id = self._tool.status_server.bot_id or "bot"
245         description = "Archive of layout-test-results from %s" % bot_id
246         # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading.
247         results_archive_file = results_archive_zip.fp
248         # Rewind the file object to start (since Mechanize won't do that automatically)
249         # See https://bugs.webkit.org/show_bug.cgi?id=54593
250         results_archive_file.seek(0)
251         # FIXME: This is a small lie to always say run-webkit-tests since Chromium uses new-run-webkit-tests.
252         # We could make this code look up the test script name off the port.
253         comment_text = "The attached test failures were seen while running run-webkit-tests on the %s.\n" % (self.name)
254         # FIXME: We could easily list the test failures from the archive here,
255         # currently callers do that separately.
256         comment_text += BotInfo(self._tool).summary_text()
257         self._tool.bugs.add_attachment_to_bug(patch.bug_id(), results_archive_file, description, filename="layout-test-results.zip", comment_text=comment_text)
258
259     def work_item_log_path(self, patch):
260         return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
261
262
263 class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate):
264     name = "commit-queue"
265
266     # AbstractPatchQueue methods
267
268     def begin_work_queue(self):
269         AbstractPatchQueue.begin_work_queue(self)
270         self.committer_validator = CommitterValidator(self._tool.bugs)
271         self._expected_failures = ExpectedFailures()
272         self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory())
273
274     def next_work_item(self):
275         return self._next_patch()
276
277     def should_proceed_with_work_item(self, patch):
278         patch_text = "rollout patch" if patch.is_rollout() else "patch"
279         self._update_status("Processing %s" % patch_text, patch)
280         return True
281
282     def process_work_item(self, patch):
283         self._cc_watchers(patch.bug_id())
284         task = CommitQueueTask(self, patch)
285         try:
286             if task.run():
287                 self._did_pass(patch)
288                 return True
289             self._did_retry(patch)
290         except ScriptError, e:
291             validator = CommitterValidator(self._tool.bugs)
292             validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e))
293             results_archive = task.results_archive_from_patch_test_run(patch)
294             if results_archive:
295                 self._upload_results_archive_for_patch(patch, results_archive)
296             self._did_fail(patch)
297
298     def _error_message_for_bug(self, status_id, script_error):
299         if not script_error.output:
300             return script_error.message_with_output()
301         results_link = self._tool.status_server.results_url_for_status(status_id)
302         return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
303
304     def handle_unexpected_error(self, patch, message):
305         self.committer_validator.reject_patch_from_commit_queue(patch.id(), message)
306
307     # CommitQueueTaskDelegate methods
308
309     def run_command(self, command):
310         self.run_webkit_patch(command)
311
312     def command_passed(self, message, patch):
313         self._update_status(message, patch=patch)
314
315     def command_failed(self, message, script_error, patch):
316         failure_log = self._log_from_script_error_for_upload(script_error)
317         return self._update_status(message, patch=patch, results_file=failure_log)
318
319     def expected_failures(self):
320         return self._expected_failures
321
322     def layout_test_results(self):
323         return self._layout_test_results_reader.results()
324
325     def archive_last_layout_test_results(self, patch):
326         return self._layout_test_results_reader.archive(patch)
327
328     def build_style(self):
329         return "both"
330
331     def refetch_patch(self, patch):
332         return self._tool.bugs.fetch_attachment(patch.id())
333
334     def report_flaky_tests(self, patch, flaky_test_results, results_archive=None):
335         reporter = FlakyTestReporter(self._tool, self.name)
336         reporter.report_flaky_tests(patch, flaky_test_results, results_archive)
337
338     # StepSequenceErrorHandler methods
339
340     def handle_script_error(cls, tool, state, script_error):
341         # Hitting this error handler should be pretty rare.  It does occur,
342         # however, when a patch no longer applies to top-of-tree in the final
343         # land step.
344         log(script_error.message_with_output())
345
346     @classmethod
347     def handle_checkout_needs_update(cls, tool, state, options, error):
348         message = "Tests passed, but commit failed (checkout out of date).  Updating, then landing without building or re-running tests."
349         tool.status_server.update_status(cls.name, message, state["patch"])
350         # The only time when we find out that out checkout needs update is
351         # when we were ready to actually pull the trigger and land the patch.
352         # Rather than spinning in the master process, we retry without
353         # building or testing, which is much faster.
354         options.build = False
355         options.test = False
356         options.update = True
357         raise TryAgain()
358
359
360 class AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler):
361     """This is the base-class for the EWS queues and the style-queue."""
362     def __init__(self, options=None):
363         AbstractPatchQueue.__init__(self, options)
364
365     def review_patch(self, patch):
366         raise NotImplementedError("subclasses must implement")
367
368     # AbstractPatchQueue methods
369
370     def begin_work_queue(self):
371         AbstractPatchQueue.begin_work_queue(self)
372
373     def next_work_item(self):
374         return self._next_patch()
375
376     def should_proceed_with_work_item(self, patch):
377         raise NotImplementedError("subclasses must implement")
378
379     def process_work_item(self, patch):
380         try:
381             if not self.review_patch(patch):
382                 return False
383             self._did_pass(patch)
384             return True
385         except ScriptError, e:
386             if e.exit_code != QueueEngine.handled_error_code:
387                 self._did_fail(patch)
388             else:
389                 # The subprocess handled the error, but won't have released the patch, so we do.
390                 # FIXME: We need to simplify the rules by which _release_work_item is called.
391                 self._release_work_item(patch)
392             raise e
393
394     def handle_unexpected_error(self, patch, message):
395         log(message)
396
397     # StepSequenceErrorHandler methods
398
399     @classmethod
400     def handle_script_error(cls, tool, state, script_error):
401         log(script_error.message_with_output())
402
403
404 class StyleQueue(AbstractReviewQueue):
405     name = "style-queue"
406     def __init__(self):
407         AbstractReviewQueue.__init__(self)
408
409     def should_proceed_with_work_item(self, patch):
410         self._update_status("Checking style", patch)
411         return True
412
413     def review_patch(self, patch):
414         self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()])
415         return True
416
417     @classmethod
418     def handle_script_error(cls, tool, state, script_error):
419         is_svn_apply = script_error.command_name() == "svn-apply"
420         status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
421         if is_svn_apply:
422             QueueEngine.exit_after_handled_error(script_error)
423         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))
424         tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers)
425         exit(1)