WebKitTools/Scripts/webkitpy/tool/commands/queues.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     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 import traceback
       
    31 import os
       
    32 
       
    33 from datetime import datetime
       
    34 from optparse import make_option
       
    35 from StringIO import StringIO
       
    36 
       
    37 from webkitpy.common.net.bugzilla import CommitterValidator
       
    38 from webkitpy.common.net.statusserver import StatusServer
       
    39 from webkitpy.common.system.executive import ScriptError
       
    40 from webkitpy.common.system.deprecated_logging import error, log
       
    41 from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
       
    42 from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate
       
    43 from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
       
    44 from webkitpy.tool.grammar import pluralize
       
    45 from webkitpy.tool.multicommandtool import Command, TryAgain
       
    46 
       
    47 class AbstractQueue(Command, QueueEngineDelegate):
       
    48     watchers = [
       
    49     ]
       
    50 
       
    51     _pass_status = "Pass"
       
    52     _fail_status = "Fail"
       
    53     _error_status = "Error"
       
    54 
       
    55     def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
       
    56         options_list = (options or []) + [
       
    57             make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
       
    58             make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."),
       
    59         ]
       
    60         Command.__init__(self, "Run the %s" % self.name, options=options_list)
       
    61         self._iteration_count = 0
       
    62 
       
    63     def _cc_watchers(self, bug_id):
       
    64         try:
       
    65             self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
       
    66         except Exception, e:
       
    67             traceback.print_exc()
       
    68             log("Failed to CC watchers.")
       
    69 
       
    70     def run_webkit_patch(self, args):
       
    71         webkit_patch_args = [self.tool.path()]
       
    72         # FIXME: This is a hack, we should have a more general way to pass global options.
       
    73         # FIXME: We must always pass global options and their value in one argument
       
    74         # because our global option code looks for the first argument which does
       
    75         # not begin with "-" and assumes that is the command name.
       
    76         webkit_patch_args += ["--status-host=%s" % self.tool.status_server.host]
       
    77         webkit_patch_args.extend(args)
       
    78         return self.tool.executive.run_and_throw_if_fail(webkit_patch_args)
       
    79 
       
    80     def _log_directory(self):
       
    81         return "%s-logs" % self.name
       
    82 
       
    83     # QueueEngineDelegate methods
       
    84 
       
    85     def queue_log_path(self):
       
    86         return os.path.join(self._log_directory(), "%s.log" % self.name)
       
    87 
       
    88     def work_item_log_path(self, work_item):
       
    89         raise NotImplementedError, "subclasses must implement"
       
    90 
       
    91     def begin_work_queue(self):
       
    92         log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root))
       
    93         if self.options.confirm:
       
    94             response = self.tool.user.prompt("Are you sure?  Type \"yes\" to continue: ")
       
    95             if (response != "yes"):
       
    96                 error("User declined.")
       
    97         log("Running WebKit %s." % self.name)
       
    98 
       
    99     def should_continue_work_queue(self):
       
   100         self._iteration_count += 1
       
   101         return not self.options.iterations or self._iteration_count <= self.options.iterations
       
   102 
       
   103     def next_work_item(self):
       
   104         raise NotImplementedError, "subclasses must implement"
       
   105 
       
   106     def should_proceed_with_work_item(self, work_item):
       
   107         raise NotImplementedError, "subclasses must implement"
       
   108 
       
   109     def process_work_item(self, work_item):
       
   110         raise NotImplementedError, "subclasses must implement"
       
   111 
       
   112     def handle_unexpected_error(self, work_item, message):
       
   113         raise NotImplementedError, "subclasses must implement"
       
   114 
       
   115     # Command methods
       
   116 
       
   117     def execute(self, options, args, tool, engine=QueueEngine):
       
   118         self.options = options
       
   119         self.tool = tool
       
   120         return engine(self.name, self, self.tool.wakeup_event).run()
       
   121 
       
   122     @classmethod
       
   123     def _update_status_for_script_error(cls, tool, state, script_error, is_error=False):
       
   124         message = str(script_error)
       
   125         if is_error:
       
   126             message = "Error: %s" % message
       
   127         output = script_error.message_with_output(output_limit=1024*1024) # 1MB
       
   128         # We pre-encode the string to a byte array before passing it
       
   129         # to status_server, because ClientForm (part of mechanize)
       
   130         # wants a file-like object with pre-encoded data.
       
   131         return tool.status_server.update_status(cls.name, message, state["patch"], StringIO(output.encode("utf-8")))
       
   132 
       
   133 
       
   134 class AbstractPatchQueue(AbstractQueue):
       
   135     def _update_status(self, message, patch=None, results_file=None):
       
   136         self.tool.status_server.update_status(self.name, message, patch, results_file)
       
   137 
       
   138     def _update_work_items(self, patch_ids):
       
   139         self.tool.status_server.update_work_items(self.name, patch_ids)
       
   140 
       
   141     def _did_pass(self, patch):
       
   142         self._update_status(self._pass_status, patch)
       
   143 
       
   144     def _did_fail(self, patch):
       
   145         self._update_status(self._fail_status, patch)
       
   146 
       
   147     def _did_error(self, patch, reason):
       
   148         message = "%s: %s" % (self._error_status, reason)
       
   149         self._update_status(message, patch)
       
   150 
       
   151     def work_item_log_path(self, patch):
       
   152         return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
       
   153 
       
   154     def log_progress(self, patch_ids):
       
   155         log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
       
   156 
       
   157 
       
   158 class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler):
       
   159     name = "commit-queue"
       
   160     def __init__(self):
       
   161         AbstractPatchQueue.__init__(self)
       
   162 
       
   163     # AbstractPatchQueue methods
       
   164 
       
   165     def begin_work_queue(self):
       
   166         AbstractPatchQueue.begin_work_queue(self)
       
   167         self.committer_validator = CommitterValidator(self.tool.bugs)
       
   168 
       
   169     def _validate_patches_in_commit_queue(self):
       
   170         # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers.
       
   171         bug_ids = self.tool.bugs.queries.fetch_bug_ids_from_commit_queue()
       
   172         all_patches = sum([self.tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) for bug_id in bug_ids], [])
       
   173         return self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches)
       
   174 
       
   175     def _patch_cmp(self, a, b):
       
   176         # Sort first by is_rollout, then by attach_date.
       
   177         # Reversing the order so that is_rollout is first.
       
   178         rollout_cmp = cmp(b.is_rollout(), a.is_rollout())
       
   179         if (rollout_cmp != 0):
       
   180             return rollout_cmp
       
   181         return cmp(a.attach_date(), b.attach_date())
       
   182 
       
   183     def next_work_item(self):
       
   184         patches = self._validate_patches_in_commit_queue()
       
   185         patches = sorted(patches, self._patch_cmp)
       
   186         self._update_work_items([patch.id() for patch in patches])
       
   187         if not patches:
       
   188             self._update_status("Empty queue")
       
   189             return None
       
   190         # Only bother logging if we have patches in the queue.
       
   191         self.log_progress([patch.id() for patch in patches])
       
   192         return patches[0]
       
   193 
       
   194     def _can_build_and_test(self):
       
   195         try:
       
   196             self.run_webkit_patch([
       
   197                 "build-and-test",
       
   198                 "--force-clean",
       
   199                 "--build",
       
   200                 "--test",
       
   201                 "--non-interactive",
       
   202                 "--no-update",
       
   203                 "--build-style=both",
       
   204                 "--quiet"])
       
   205         except ScriptError, e:
       
   206             self._update_status("Unable to successfully build and test", None)
       
   207             return False
       
   208         return True
       
   209 
       
   210     def should_proceed_with_work_item(self, patch):
       
   211         patch_text = "rollout patch" if patch.is_rollout() else "patch"
       
   212         self._update_status("Landing %s" % patch_text, patch)
       
   213         return True
       
   214 
       
   215     def _land(self, patch, first_run=False):
       
   216         try:
       
   217             args = [
       
   218                 "land-attachment",
       
   219                 "--force-clean",
       
   220                 "--build",
       
   221                 "--non-interactive",
       
   222                 "--ignore-builders",
       
   223                 "--build-style=both",
       
   224                 "--quiet",
       
   225                 patch.id()
       
   226             ]
       
   227             # We don't bother to run tests for rollouts as that makes them too slow.
       
   228             if not patch.is_rollout():
       
   229                 args.append("--test")
       
   230             if not first_run:
       
   231                 # The first time through, we don't reject the patch from the
       
   232                 # commit queue because we want to make sure we can build and
       
   233                 # test ourselves. However, the second time through, we
       
   234                 # register ourselves as the parent-command so we can reject
       
   235                 # the patch on failure.
       
   236                 args.append("--parent-command=commit-queue")
       
   237                 # The second time through, we also don't want to update so we
       
   238                 # know we're testing the same revision that we successfully
       
   239                 # built and tested.
       
   240                 args.append("--no-update")
       
   241             self.run_webkit_patch(args)
       
   242             self._did_pass(patch)
       
   243             return True
       
   244         except ScriptError, e:
       
   245             if first_run:
       
   246                 return False
       
   247             self._did_fail(patch)
       
   248             raise
       
   249 
       
   250     def process_work_item(self, patch):
       
   251         self._cc_watchers(patch.bug_id())
       
   252         if not self._land(patch, first_run=True):
       
   253             # The patch failed to land, but the bots were green. It's possible
       
   254             # that the bots were behind. To check that case, we try to build and
       
   255             # test ourselves.
       
   256             if not self._can_build_and_test():
       
   257                 return False
       
   258             # Hum, looks like the patch is actually bad. Of course, we could
       
   259             # have been bitten by a flaky test the first time around.  We try
       
   260             # to land again.  If it fails a second time, we're pretty sure its
       
   261             # a bad test and re can reject it outright.
       
   262             self._land(patch)
       
   263         return True
       
   264 
       
   265     def handle_unexpected_error(self, patch, message):
       
   266         self.committer_validator.reject_patch_from_commit_queue(patch.id(), message)
       
   267 
       
   268     # StepSequenceErrorHandler methods
       
   269     @staticmethod
       
   270     def _error_message_for_bug(tool, status_id, script_error):
       
   271         if not script_error.output:
       
   272             return script_error.message_with_output()
       
   273         results_link = tool.status_server.results_url_for_status(status_id)
       
   274         return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
       
   275 
       
   276     @classmethod
       
   277     def handle_script_error(cls, tool, state, script_error):
       
   278         status_id = cls._update_status_for_script_error(tool, state, script_error)
       
   279         validator = CommitterValidator(tool.bugs)
       
   280         validator.reject_patch_from_commit_queue(state["patch"].id(), cls._error_message_for_bug(tool, status_id, script_error))
       
   281 
       
   282     @classmethod
       
   283     def handle_checkout_needs_update(cls, tool, state, options, error):
       
   284         # The only time when we find out that out checkout needs update is
       
   285         # when we were ready to actually pull the trigger and land the patch.
       
   286         # Rather than spinning in the master process, we retry without
       
   287         # building or testing, which is much faster.
       
   288         options.build = False
       
   289         options.test = False
       
   290         options.update = True
       
   291         raise TryAgain()
       
   292 
       
   293 
       
   294 class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler):
       
   295     name = "rietveld-upload-queue"
       
   296 
       
   297     def __init__(self):
       
   298         AbstractPatchQueue.__init__(self)
       
   299 
       
   300     # AbstractPatchQueue methods
       
   301 
       
   302     def next_work_item(self):
       
   303         patch_id = self.tool.bugs.queries.fetch_first_patch_from_rietveld_queue()
       
   304         if patch_id:
       
   305             return patch_id
       
   306         self._update_status("Empty queue")
       
   307 
       
   308     def should_proceed_with_work_item(self, patch):
       
   309         self._update_status("Uploading patch", patch)
       
   310         return True
       
   311 
       
   312     def process_work_item(self, patch):
       
   313         try:
       
   314             self.run_webkit_patch(["post-attachment-to-rietveld", "--force-clean", "--non-interactive", "--parent-command=rietveld-upload-queue", patch.id()])
       
   315             self._did_pass(patch)
       
   316             return True
       
   317         except ScriptError, e:
       
   318             if e.exit_code != QueueEngine.handled_error_code:
       
   319                 self._did_fail(patch)
       
   320             raise e
       
   321 
       
   322     @classmethod
       
   323     def _reject_patch(cls, tool, patch_id):
       
   324         tool.bugs.set_flag_on_attachment(patch_id, "in-rietveld", "-")
       
   325 
       
   326     def handle_unexpected_error(self, patch, message):
       
   327         log(message)
       
   328         self._reject_patch(self.tool, patch.id())
       
   329 
       
   330     # StepSequenceErrorHandler methods
       
   331 
       
   332     @classmethod
       
   333     def handle_script_error(cls, tool, state, script_error):
       
   334         log(script_error.message_with_output())
       
   335         cls._update_status_for_script_error(tool, state, script_error)
       
   336         cls._reject_patch(tool, state["patch"].id())
       
   337 
       
   338 
       
   339 class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler):
       
   340     def __init__(self, options=None):
       
   341         AbstractPatchQueue.__init__(self, options)
       
   342 
       
   343     def review_patch(self, patch):
       
   344         raise NotImplementedError, "subclasses must implement"
       
   345 
       
   346     # PersistentPatchCollectionDelegate methods
       
   347 
       
   348     def collection_name(self):
       
   349         return self.name
       
   350 
       
   351     def fetch_potential_patch_ids(self):
       
   352         return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue()
       
   353 
       
   354     def status_server(self):
       
   355         return self.tool.status_server
       
   356 
       
   357     def is_terminal_status(self, status):
       
   358         return status == "Pass" or status == "Fail" or status.startswith("Error:")
       
   359 
       
   360     # AbstractPatchQueue methods
       
   361 
       
   362     def begin_work_queue(self):
       
   363         AbstractPatchQueue.begin_work_queue(self)
       
   364         self._patches = PersistentPatchCollection(self)
       
   365 
       
   366     def next_work_item(self):
       
   367         patch_id = self._patches.next()
       
   368         if patch_id:
       
   369             return self.tool.bugs.fetch_attachment(patch_id)
       
   370         self._update_status("Empty queue")
       
   371 
       
   372     def should_proceed_with_work_item(self, patch):
       
   373         raise NotImplementedError, "subclasses must implement"
       
   374 
       
   375     def process_work_item(self, patch):
       
   376         try:
       
   377             if not self.review_patch(patch):
       
   378                 return False
       
   379             self._did_pass(patch)
       
   380             return True
       
   381         except ScriptError, e:
       
   382             if e.exit_code != QueueEngine.handled_error_code:
       
   383                 self._did_fail(patch)
       
   384             raise e
       
   385 
       
   386     def handle_unexpected_error(self, patch, message):
       
   387         log(message)
       
   388 
       
   389     # StepSequenceErrorHandler methods
       
   390 
       
   391     @classmethod
       
   392     def handle_script_error(cls, tool, state, script_error):
       
   393         log(script_error.message_with_output())
       
   394 
       
   395 
       
   396 class StyleQueue(AbstractReviewQueue):
       
   397     name = "style-queue"
       
   398     def __init__(self):
       
   399         AbstractReviewQueue.__init__(self)
       
   400 
       
   401     def should_proceed_with_work_item(self, patch):
       
   402         self._update_status("Checking style", patch)
       
   403         return True
       
   404 
       
   405     def review_patch(self, patch):
       
   406         self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()])
       
   407         return True
       
   408 
       
   409     @classmethod
       
   410     def handle_script_error(cls, tool, state, script_error):
       
   411         is_svn_apply = script_error.command_name() == "svn-apply"
       
   412         status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
       
   413         if is_svn_apply:
       
   414             QueueEngine.exit_after_handled_error(script_error)
       
   415         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))
       
   416         tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers)
       
   417         exit(1)