Home | History | Annotate | Download | only in commands
      1 # Copyright (C) 2009 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 import os
     30 import StringIO
     31 
     32 from webkitpy.common.checkout.scm import CheckoutNeedsUpdate
     33 from webkitpy.common.net.bugzilla import Attachment
     34 from webkitpy.common.system.filesystem_mock import MockFileSystem
     35 from webkitpy.common.system.outputcapture import OutputCapture
     36 from webkitpy.layout_tests.layout_package import test_results
     37 from webkitpy.layout_tests.layout_package import test_failures
     38 from webkitpy.thirdparty.mock import Mock
     39 from webkitpy.tool.commands.commandtest import CommandsTest
     40 from webkitpy.tool.commands.queues import *
     41 from webkitpy.tool.commands.queuestest import QueuesTest
     42 from webkitpy.tool.commands.stepsequence import StepSequence
     43 from webkitpy.tool.mocktool import MockTool, MockSCM, MockStatusServer
     44 
     45 
     46 class TestQueue(AbstractPatchQueue):
     47     name = "test-queue"
     48 
     49 
     50 class TestReviewQueue(AbstractReviewQueue):
     51     name = "test-review-queue"
     52 
     53 
     54 class TestFeederQueue(FeederQueue):
     55     _sleep_duration = 0
     56 
     57 
     58 class AbstractQueueTest(CommandsTest):
     59     def test_log_directory(self):
     60         self.assertEquals(TestQueue()._log_directory(), os.path.join("..", "test-queue-logs"))
     61 
     62     def _assert_run_webkit_patch(self, run_args, port=None):
     63         queue = TestQueue()
     64         tool = MockTool()
     65         tool.status_server.bot_id = "gort"
     66         tool.executive = Mock()
     67         queue.bind_to_tool(tool)
     68         queue._options = Mock()
     69         queue._options.port = port
     70 
     71         queue.run_webkit_patch(run_args)
     72         expected_run_args = ["echo", "--status-host=example.com", "--bot-id=gort"]
     73         if port:
     74             expected_run_args.append("--port=%s" % port)
     75         expected_run_args.extend(run_args)
     76         tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
     77 
     78     def test_run_webkit_patch(self):
     79         self._assert_run_webkit_patch([1])
     80         self._assert_run_webkit_patch(["one", 2])
     81         self._assert_run_webkit_patch([1], port="mockport")
     82 
     83     def test_iteration_count(self):
     84         queue = TestQueue()
     85         queue._options = Mock()
     86         queue._options.iterations = 3
     87         self.assertTrue(queue.should_continue_work_queue())
     88         self.assertTrue(queue.should_continue_work_queue())
     89         self.assertTrue(queue.should_continue_work_queue())
     90         self.assertFalse(queue.should_continue_work_queue())
     91 
     92     def test_no_iteration_count(self):
     93         queue = TestQueue()
     94         queue._options = Mock()
     95         self.assertTrue(queue.should_continue_work_queue())
     96         self.assertTrue(queue.should_continue_work_queue())
     97         self.assertTrue(queue.should_continue_work_queue())
     98         self.assertTrue(queue.should_continue_work_queue())
     99 
    100     def _assert_log_message(self, script_error, log_message):
    101         failure_log = AbstractQueue._log_from_script_error_for_upload(script_error, output_limit=10)
    102         self.assertTrue(failure_log.read(), log_message)
    103 
    104     def test_log_from_script_error_for_upload(self):
    105         self._assert_log_message(ScriptError("test"), "test")
    106         # In python 2.5 unicode(Exception) is busted. See:
    107         # http://bugs.python.org/issue2517
    108         # With no good workaround, we just ignore these tests.
    109         if not hasattr(Exception, "__unicode__"):
    110             return
    111 
    112         unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
    113         utf8_tor = unicode_tor.encode("utf-8")
    114         self._assert_log_message(ScriptError(unicode_tor), utf8_tor)
    115         script_error = ScriptError(unicode_tor, output=unicode_tor)
    116         expected_output = "%s\nLast %s characters of output:\n%s" % (utf8_tor, 10, utf8_tor[-10:])
    117         self._assert_log_message(script_error, expected_output)
    118 
    119 
    120 class FeederQueueTest(QueuesTest):
    121     def test_feeder_queue(self):
    122         queue = TestFeederQueue()
    123         tool = MockTool(log_executive=True)
    124         expected_stderr = {
    125             "begin_work_queue": self._default_begin_work_queue_stderr("feeder-queue", MockSCM.fake_checkout_root),
    126             "should_proceed_with_work_item": "",
    127             "next_work_item": "",
    128             "process_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer (at] example.com)
    129 Warning, attachment 128 on bug 42 has invalid committer (non-committer (at] example.com)
    130 MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting attachment 128 from commit-queue.' and additional comment 'non-committer (at] example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/Tools/Scripts/webkitpy/common/config/committers.py.
    131 
    132 - If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
    133 
    134 - If you have committer rights please correct the error in Tools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed).  The commit-queue restarts itself every 2 hours.  After restart the commit-queue will correctly respect your committer rights.'
    135 MOCK: update_work_items: commit-queue [106, 197]
    136 Feeding commit-queue items [106, 197]
    137 Feeding EWS (1 r? patch, 1 new)
    138 MOCK: submit_to_ews: 103
    139 """,
    140             "handle_unexpected_error": "Mock error message\n",
    141         }
    142         self.assert_queue_outputs(queue, tool=tool, expected_stderr=expected_stderr)
    143 
    144 
    145 class AbstractPatchQueueTest(CommandsTest):
    146     def test_next_patch(self):
    147         queue = AbstractPatchQueue()
    148         tool = MockTool()
    149         queue.bind_to_tool(tool)
    150         queue._options = Mock()
    151         queue._options.port = None
    152         self.assertEquals(queue._next_patch(), None)
    153         tool.status_server = MockStatusServer(work_items=[2, 197])
    154         expected_stdout = "MOCK: fetch_attachment: 2 is not a known attachment id\n"  # A mock-only message to prevent us from making mistakes.
    155         expected_stderr = "MOCK: release_work_item: None 2\n"
    156         patch_id = OutputCapture().assert_outputs(self, queue._next_patch, [], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
    157         self.assertEquals(patch_id, None)  # 2 is an invalid patch id
    158         self.assertEquals(queue._next_patch().id(), 197)
    159 
    160 
    161 class NeedsUpdateSequence(StepSequence):
    162     def _run(self, tool, options, state):
    163         raise CheckoutNeedsUpdate([], 1, "", None)
    164 
    165 
    166 class AlwaysCommitQueueTool(object):
    167     def __init__(self):
    168         self.status_server = MockStatusServer()
    169 
    170     def command_by_name(self, name):
    171         return CommitQueue
    172 
    173 
    174 class SecondThoughtsCommitQueue(CommitQueue):
    175     def __init__(self):
    176         self._reject_patch = False
    177         CommitQueue.__init__(self)
    178 
    179     def run_command(self, command):
    180         # We want to reject the patch after the first validation,
    181         # so wait to reject it until after some other command has run.
    182         self._reject_patch = True
    183         return CommitQueue.run_command(self, command)
    184 
    185     def refetch_patch(self, patch):
    186         if not self._reject_patch:
    187             return self._tool.bugs.fetch_attachment(patch.id())
    188 
    189         attachment_dictionary = {
    190             "id": patch.id(),
    191             "bug_id": patch.bug_id(),
    192             "name": "Rejected",
    193             "is_obsolete": True,
    194             "is_patch": False,
    195             "review": "-",
    196             "reviewer_email": "foo (at] bar.com",
    197             "commit-queue": "-",
    198             "committer_email": "foo (at] bar.com",
    199             "attacher_email": "Contributer1",
    200         }
    201         return Attachment(attachment_dictionary, None)
    202 
    203 
    204 class CommitQueueTest(QueuesTest):
    205     def _mock_test_result(self, testname):
    206         return test_results.TestResult(testname, [test_failures.FailureTextMismatch()])
    207 
    208     def test_commit_queue(self):
    209         expected_stderr = {
    210             "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
    211             "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n",
    212             "next_work_item": "",
    213             "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory
    214 MOCK: update_status: commit-queue Updated working directory
    215 MOCK: update_status: commit-queue Applied patch
    216 MOCK: update_status: commit-queue Built patch
    217 MOCK: update_status: commit-queue Passed tests
    218 MOCK: update_status: commit-queue Landed patch
    219 MOCK: update_status: commit-queue Pass
    220 MOCK: release_work_item: commit-queue 197
    221 """,
    222             "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 from commit-queue.' and additional comment 'Mock error message'\n",
    223             "handle_script_error": "ScriptError error message\n",
    224         }
    225         self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr)
    226 
    227     def test_commit_queue_failure(self):
    228         expected_stderr = {
    229             "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
    230             "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n",
    231             "next_work_item": "",
    232             "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory
    233 MOCK: update_status: commit-queue Updated working directory
    234 MOCK: update_status: commit-queue Patch does not apply
    235 MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 from commit-queue.' and additional comment 'MOCK script error'
    236 MOCK: update_status: commit-queue Fail
    237 MOCK: release_work_item: commit-queue 197
    238 """,
    239             "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 from commit-queue.' and additional comment 'Mock error message'\n",
    240             "handle_script_error": "ScriptError error message\n",
    241         }
    242         queue = CommitQueue()
    243 
    244         def mock_run_webkit_patch(command):
    245             if command == ['clean'] or command == ['update']:
    246                 # We want cleaning to succeed so we can error out on a step
    247                 # that causes the commit-queue to reject the patch.
    248                 return
    249             raise ScriptError('MOCK script error')
    250 
    251         queue.run_webkit_patch = mock_run_webkit_patch
    252         self.assert_queue_outputs(queue, expected_stderr=expected_stderr)
    253 
    254     def test_rollout(self):
    255         tool = MockTool(log_executive=True)
    256         tool.filesystem.write_text_file('/mock/results.html', '')  # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem.
    257         tool.buildbot.light_tree_on_fire()
    258         expected_stderr = {
    259             "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
    260             "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n",
    261             "next_work_item": "",
    262             "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean']
    263 MOCK: update_status: commit-queue Cleaned working directory
    264 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update']
    265 MOCK: update_status: commit-queue Updated working directory
    266 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 197]
    267 MOCK: update_status: commit-queue Applied patch
    268 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both']
    269 MOCK: update_status: commit-queue Built patch
    270 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
    271 MOCK: update_status: commit-queue Passed tests
    272 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 197]
    273 MOCK: update_status: commit-queue Landed patch
    274 MOCK: update_status: commit-queue Pass
    275 MOCK: release_work_item: commit-queue 197
    276 """,
    277             "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 from commit-queue.' and additional comment 'Mock error message'\n",
    278             "handle_script_error": "ScriptError error message\n",
    279         }
    280         self.assert_queue_outputs(CommitQueue(), tool=tool, expected_stderr=expected_stderr)
    281 
    282     def test_rollout_lands(self):
    283         tool = MockTool(log_executive=True)
    284         tool.buildbot.light_tree_on_fire()
    285         rollout_patch = tool.bugs.fetch_attachment(106)  # _patch6, a rollout patch.
    286         assert(rollout_patch.is_rollout())
    287         expected_stderr = {
    288             "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
    289             "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing rollout patch\n",
    290             "next_work_item": "",
    291             "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean']
    292 MOCK: update_status: commit-queue Cleaned working directory
    293 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update']
    294 MOCK: update_status: commit-queue Updated working directory
    295 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 106]
    296 MOCK: update_status: commit-queue Applied patch
    297 MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 106]
    298 MOCK: update_status: commit-queue Landed patch
    299 MOCK: update_status: commit-queue Pass
    300 MOCK: release_work_item: commit-queue 106
    301 """,
    302             "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '106' with comment 'Rejecting attachment 106 from commit-queue.' and additional comment 'Mock error message'\n",
    303             "handle_script_error": "ScriptError error message\n",
    304         }
    305         self.assert_queue_outputs(CommitQueue(), tool=tool, work_item=rollout_patch, expected_stderr=expected_stderr)
    306 
    307     def test_auto_retry(self):
    308         queue = CommitQueue()
    309         options = Mock()
    310         options.parent_command = "commit-queue"
    311         tool = AlwaysCommitQueueTool()
    312         sequence = NeedsUpdateSequence(None)
    313 
    314         expected_stderr = "Commit failed because the checkout is out of date.  Please update and try again.\nMOCK: update_status: commit-queue Tests passed, but commit failed (checkout out of date).  Updating, then landing without building or re-running tests.\n"
    315         state = {'patch': None}
    316         OutputCapture().assert_outputs(self, sequence.run_and_handle_errors, [tool, options, state], expected_exception=TryAgain, expected_stderr=expected_stderr)
    317 
    318         self.assertEquals(options.update, True)
    319         self.assertEquals(options.build, False)
    320         self.assertEquals(options.test, False)
    321 
    322     def test_manual_reject_during_processing(self):
    323         queue = SecondThoughtsCommitQueue()
    324         queue.bind_to_tool(MockTool())
    325         queue._tool.filesystem.write_text_file('/mock/results.html', '')  # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem.
    326         queue._options = Mock()
    327         queue._options.port = None
    328         expected_stderr = """MOCK: update_status: commit-queue Cleaned working directory
    329 MOCK: update_status: commit-queue Updated working directory
    330 MOCK: update_status: commit-queue Applied patch
    331 MOCK: update_status: commit-queue Built patch
    332 MOCK: update_status: commit-queue Passed tests
    333 MOCK: update_status: commit-queue Retry
    334 MOCK: release_work_item: commit-queue 197
    335 """
    336         OutputCapture().assert_outputs(self, queue.process_work_item, [QueuesTest.mock_work_item], expected_stderr=expected_stderr)
    337 
    338     def test_report_flaky_tests(self):
    339         queue = CommitQueue()
    340         queue.bind_to_tool(MockTool())
    341         expected_stderr = """MOCK bug comment: bug_id=76, cc=None
    342 --- Begin comment ---
    343 The commit-queue just saw foo/bar.html flake (Text diff mismatch) while processing attachment 197 on bug 42.
    344 Port: MockPort  Platform: MockPlatform 1.0
    345 --- End comment ---
    346 
    347 MOCK add_attachment_to_bug: bug_id=76, description=Failure diff from bot filename=failure.diff
    348 MOCK bug comment: bug_id=76, cc=None
    349 --- Begin comment ---
    350 The commit-queue just saw bar/baz.html flake (Text diff mismatch) while processing attachment 197 on bug 42.
    351 Port: MockPort  Platform: MockPlatform 1.0
    352 --- End comment ---
    353 
    354 MOCK add_attachment_to_bug: bug_id=76, description=Archive of layout-test-results from bot filename=layout-test-results.zip
    355 MOCK bug comment: bug_id=42, cc=None
    356 --- Begin comment ---
    357 The commit-queue encountered the following flaky tests while processing attachment 197:
    358 
    359 foo/bar.html bug 76 (author: abarth (at] webkit.org)
    360 bar/baz.html bug 76 (author: abarth (at] webkit.org)
    361 The commit-queue is continuing to process your patch.
    362 --- End comment ---
    363 
    364 """
    365         test_names = ["foo/bar.html", "bar/baz.html"]
    366         test_results = [self._mock_test_result(name) for name in test_names]
    367 
    368         class MockZipFile(object):
    369             def __init__(self):
    370                 self.fp = StringIO()
    371 
    372             def read(self, path):
    373                 return ""
    374 
    375             def namelist(self):
    376                 # This is intentionally missing one diffs.txt to exercise the "upload the whole zip" codepath.
    377                 return ['foo/bar-diffs.txt']
    378 
    379         OutputCapture().assert_outputs(self, queue.report_flaky_tests, [QueuesTest.mock_work_item, test_results, MockZipFile()], expected_stderr=expected_stderr)
    380 
    381     def test_missing_layout_test_results(self):
    382         queue = CommitQueue()
    383         tool = MockTool()
    384         results_path = '/mock/results.html'
    385         tool.filesystem = MockFileSystem({results_path: None})
    386         queue.bind_to_tool(tool)
    387         # Make sure that our filesystem mock functions as we expect.
    388         self.assertRaises(IOError, tool.filesystem.read_text_file, results_path)
    389         # layout_test_results shouldn't raise even if the results.html file is missing.
    390         self.assertEquals(queue.layout_test_results(), None)
    391 
    392     def test_layout_test_results(self):
    393         queue = CommitQueue()
    394         queue.bind_to_tool(MockTool())
    395         queue._read_file_contents = lambda path: None
    396         self.assertEquals(queue.layout_test_results(), None)
    397         queue._read_file_contents = lambda path: ""
    398         self.assertEquals(queue.layout_test_results(), None)
    399         queue._create_layout_test_results = lambda: LayoutTestResults([])
    400         results = queue.layout_test_results()
    401         self.assertNotEquals(results, None)
    402         self.assertEquals(results.failure_limit_count(), 10)  # This value matches RunTests.NON_INTERACTIVE_FAILURE_LIMIT_COUNT
    403 
    404     def test_archive_last_layout_test_results(self):
    405         queue = CommitQueue()
    406         queue.bind_to_tool(MockTool())
    407         patch = queue._tool.bugs.fetch_attachment(128)
    408         # This is just to test that the method doesn't raise.
    409         queue.archive_last_layout_test_results(patch)
    410 
    411     def test_upload_results_archive_for_patch(self):
    412         queue = CommitQueue()
    413         queue.bind_to_tool(MockTool())
    414         patch = queue._tool.bugs.fetch_attachment(128)
    415         expected_stderr = """MOCK add_attachment_to_bug: bug_id=42, description=Archive of layout-test-results from bot filename=layout-test-results.zip
    416 -- Begin comment --
    417 The attached test failures were seen while running run-webkit-tests on the commit-queue.
    418 Port: MockPort  Platform: MockPlatform 1.0
    419 -- End comment --
    420 """
    421         OutputCapture().assert_outputs(self, queue._upload_results_archive_for_patch, [patch, Mock()], expected_stderr=expected_stderr)
    422 
    423 
    424 class StyleQueueTest(QueuesTest):
    425     def test_style_queue(self):
    426         expected_stderr = {
    427             "begin_work_queue": self._default_begin_work_queue_stderr("style-queue", MockSCM.fake_checkout_root),
    428             "next_work_item": "",
    429             "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n",
    430             "process_work_item": "MOCK: update_status: style-queue Pass\nMOCK: release_work_item: style-queue 197\n",
    431             "handle_unexpected_error": "Mock error message\n",
    432             "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=42, cc=[]\n--- Begin comment ---\nAttachment 197 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n",
    433         }
    434         expected_exceptions = {
    435             "handle_script_error": SystemExit,
    436         }
    437         self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions)
    438