Home | History | Annotate | Download | only in bot
      1 # Copyright (c) 2010 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #     * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #     * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #     * Neither the name of Google Inc. nor the names of its
     14 # contributors may be used to endorse or promote products derived from
     15 # this software without specific prior written permission.
     16 #
     17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 from webkitpy.common.system.executive import ScriptError
     30 from webkitpy.common.net.layouttestresults import LayoutTestResults
     31 from webkitpy.tool.bot.expectedfailures import ExpectedFailures
     32 
     33 
     34 class CommitQueueTaskDelegate(object):
     35     def run_command(self, command):
     36         raise NotImplementedError("subclasses must implement")
     37 
     38     def command_passed(self, message, patch):
     39         raise NotImplementedError("subclasses must implement")
     40 
     41     def command_failed(self, message, script_error, patch):
     42         raise NotImplementedError("subclasses must implement")
     43 
     44     def refetch_patch(self, patch):
     45         raise NotImplementedError("subclasses must implement")
     46 
     47     def layout_test_results(self):
     48         raise NotImplementedError("subclasses must implement")
     49 
     50     def archive_last_layout_test_results(self, patch):
     51         raise NotImplementedError("subclasses must implement")
     52 
     53     # We could make results_archive optional, but for now it's required.
     54     def report_flaky_tests(self, patch, flaky_tests, results_archive):
     55         raise NotImplementedError("subclasses must implement")
     56 
     57 
     58 class CommitQueueTask(object):
     59     def __init__(self, delegate, patch):
     60         self._delegate = delegate
     61         self._patch = patch
     62         self._script_error = None
     63         self._results_archive_from_patch_test_run = None
     64         self._expected_failures = ExpectedFailures()
     65 
     66     def _validate(self):
     67         # Bugs might get closed, or patches might be obsoleted or r-'d while the
     68         # commit-queue is processing.
     69         self._patch = self._delegate.refetch_patch(self._patch)
     70         if self._patch.is_obsolete():
     71             return False
     72         if self._patch.bug().is_closed():
     73             return False
     74         if not self._patch.committer():
     75             return False
     76         if not self._patch.review() != "-":
     77             return False
     78         # Reviewer is not required. Missing reviewers will be caught during
     79         # the ChangeLog check during landing.
     80         return True
     81 
     82     def _run_command(self, command, success_message, failure_message):
     83         try:
     84             self._delegate.run_command(command)
     85             self._delegate.command_passed(success_message, patch=self._patch)
     86             return True
     87         except ScriptError, e:
     88             self._script_error = e
     89             self.failure_status_id = self._delegate.command_failed(failure_message, script_error=self._script_error, patch=self._patch)
     90             return False
     91 
     92     def _clean(self):
     93         return self._run_command([
     94             "clean",
     95         ],
     96         "Cleaned working directory",
     97         "Unable to clean working directory")
     98 
     99     def _update(self):
    100         # FIXME: Ideally the status server log message should include which revision we updated to.
    101         return self._run_command([
    102             "update",
    103         ],
    104         "Updated working directory",
    105         "Unable to update working directory")
    106 
    107     def _apply(self):
    108         return self._run_command([
    109             "apply-attachment",
    110             "--no-update",
    111             "--non-interactive",
    112             self._patch.id(),
    113         ],
    114         "Applied patch",
    115         "Patch does not apply")
    116 
    117     def _build(self):
    118         return self._run_command([
    119             "build",
    120             "--no-clean",
    121             "--no-update",
    122             "--build-style=both",
    123         ],
    124         "Built patch",
    125         "Patch does not build")
    126 
    127     def _build_without_patch(self):
    128         return self._run_command([
    129             "build",
    130             "--force-clean",
    131             "--no-update",
    132             "--build-style=both",
    133         ],
    134         "Able to build without patch",
    135         "Unable to build without patch")
    136 
    137     def _test(self):
    138         success = self._run_command([
    139             "build-and-test",
    140             "--no-clean",
    141             "--no-update",
    142             # Notice that we don't pass --build, which means we won't build!
    143             "--test",
    144             "--non-interactive",
    145         ],
    146         "Passed tests",
    147         "Patch does not pass tests")
    148 
    149         self._expected_failures.shrink_expected_failures(self._delegate.layout_test_results(), success)
    150         return success
    151 
    152     def _build_and_test_without_patch(self):
    153         success = self._run_command([
    154             "build-and-test",
    155             "--force-clean",
    156             "--no-update",
    157             "--build",
    158             "--test",
    159             "--non-interactive",
    160         ],
    161         "Able to pass tests without patch",
    162         "Unable to pass tests without patch (tree is red?)")
    163 
    164         self._expected_failures.shrink_expected_failures(self._delegate.layout_test_results(), success)
    165         return success
    166 
    167     def _land(self):
    168         # Unclear if this should pass --quiet or not.  If --parent-command always does the reporting, then it should.
    169         return self._run_command([
    170             "land-attachment",
    171             "--force-clean",
    172             "--ignore-builders",
    173             "--non-interactive",
    174             "--parent-command=commit-queue",
    175             self._patch.id(),
    176         ],
    177         "Landed patch",
    178         "Unable to land patch")
    179 
    180     def _report_flaky_tests(self, flaky_test_results, results_archive):
    181         self._delegate.report_flaky_tests(self._patch, flaky_test_results, results_archive)
    182 
    183     def _results_failed_different_tests(self, first, second):
    184         first_failing_tests = [] if not first else first.failing_tests()
    185         second_failing_tests = [] if not second else second.failing_tests()
    186         return first_failing_tests != second_failing_tests
    187 
    188     def _test_patch(self):
    189         if self._test():
    190             return True
    191 
    192         # Note: archive_last_layout_test_results deletes the results directory, making these calls order-sensitve.
    193         # We could remove this dependency by building the layout_test_results from the archive.
    194         first_results = self._delegate.layout_test_results()
    195         first_results_archive = self._delegate.archive_last_layout_test_results(self._patch)
    196 
    197         if self._expected_failures.failures_were_expected(first_results):
    198             return True
    199 
    200         if self._test():
    201             # Only report flaky tests if we were successful at parsing results.html and archiving results.
    202             if first_results and first_results_archive:
    203                 self._report_flaky_tests(first_results.failing_test_results(), first_results_archive)
    204             return True
    205 
    206         second_results = self._delegate.layout_test_results()
    207         if self._results_failed_different_tests(first_results, second_results):
    208             # We could report flaky tests here, but we would need to be careful
    209             # to use similar checks to ExpectedFailures._can_trust_results
    210             # to make sure we don't report constant failures as flakes when
    211             # we happen to hit the --exit-after-N-failures limit.
    212             # See https://bugs.webkit.org/show_bug.cgi?id=51272
    213             return False
    214 
    215         # Archive (and remove) second results so layout_test_results() after
    216         # build_and_test_without_patch won't use second results instead of the clean-tree results.
    217         second_results_archive = self._delegate.archive_last_layout_test_results(self._patch)
    218 
    219         if self._build_and_test_without_patch():
    220             # The error from the previous ._test() run is real, report it.
    221             return self.report_failure(first_results_archive)
    222 
    223         clean_tree_results = self._delegate.layout_test_results()
    224         self._expected_failures.grow_expected_failures(clean_tree_results)
    225 
    226         return False  # Tree must be redder than we expected, just retry later.
    227 
    228     def results_archive_from_patch_test_run(self, patch):
    229         assert(self._patch.id() == patch.id())  # CommitQueueTask is not currently re-useable.
    230         return self._results_archive_from_patch_test_run
    231 
    232     def report_failure(self, results_archive=None):
    233         if not self._validate():
    234             return False
    235         self._results_archive_from_patch_test_run = results_archive
    236         raise self._script_error
    237 
    238     def run(self):
    239         if not self._validate():
    240             return False
    241         if not self._clean():
    242             return False
    243         if not self._update():
    244             return False
    245         if not self._apply():
    246             return self.report_failure()
    247         if not self._patch.is_rollout():
    248             if not self._build():
    249                 if not self._build_without_patch():
    250                     return False
    251                 return self.report_failure()
    252             if not self._test_patch():
    253                 return False
    254         # Make sure the patch is still valid before landing (e.g., make sure
    255         # no one has set commit-queue- since we started working on the patch.)
    256         if not self._validate():
    257             return False
    258         # FIXME: We should understand why the land failure occured and retry if possible.
    259         if not self._land():
    260             return self.report_failure()
    261         return True
    262