| # -*- coding: utf-8 -*- |
| # Copyright 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Test suite for tree_status.py""" |
| |
| from __future__ import print_function |
| |
| import mock |
| import time |
| import urllib |
| |
| from chromite.lib import config_lib_unittest |
| from chromite.lib import constants |
| from chromite.lib import cros_test_lib |
| from chromite.lib import timeout_util |
| from chromite.lib import tree_status |
| |
| |
| # pylint: disable=protected-access |
| |
| |
| class TestTreeStatus(cros_test_lib.MockTestCase): |
| """Tests TreeStatus method in cros_build_lib.""" |
| |
| status_url = 'https://chromiumos-status.appspot.com/current?format=json' |
| |
| def setUp(self): |
| mock_site_config = config_lib_unittest.MockSiteConfig() |
| # Add a couple other builders so we have more than one. |
| mock_site_config.Add('x86-generic-paladin') |
| mock_site_config.Add('arm-generic-paladin') |
| self.site_config = mock_site_config |
| |
| def _TreeStatusFile(self, message, general_state): |
| """Returns a file-like object with the status message writtin in it.""" |
| json = '{"message": "%s", "general_state": "%s"}' % (message, general_state) |
| return mock.MagicMock(json=json, getcode=lambda: 200, read=lambda: json) |
| |
| def _SetupMockTreeStatusResponses(self, |
| final_tree_status='Tree is open.', |
| final_general_state=constants.TREE_OPEN, |
| rejected_tree_status='Tree is closed.', |
| rejected_general_state= |
| constants.TREE_CLOSED, |
| rejected_status_count=0, |
| retries_500=0, |
| output_final_status=True): |
| """Mocks out urllib.urlopen commands to simulate a given tree status. |
| |
| Args: |
| final_tree_status: The final value of tree status that will be returned |
| by urlopen. |
| final_general_state: The final value of 'general_state' that will be |
| returned by urlopen. |
| rejected_tree_status: An intermediate value of tree status that will be |
| returned by urlopen and retried upon. |
| rejected_general_state: An intermediate value of 'general_state' that |
| will be returned by urlopen and retried upon. |
| rejected_status_count: The number of times urlopen will return the |
| rejected state. |
| retries_500: The number of times urlopen will fail with a 500 code. |
| output_final_status: If True, the status given by final_tree_status and |
| final_general_state will be the last status returned by urlopen. If |
| False, final_tree_status will never be returned, and instead an |
| unlimited number of times rejected_response will be returned. |
| """ |
| final_response = self._TreeStatusFile(final_tree_status, |
| final_general_state) |
| rejected_response = self._TreeStatusFile(rejected_tree_status, |
| rejected_general_state) |
| |
| error_500_response = mock.MagicMock(getcode=lambda: 500) |
| return_value = [error_500_response] * retries_500 |
| |
| if output_final_status: |
| return_value += [rejected_response] * rejected_status_count |
| return_value += [final_response] |
| else: |
| return_value += [rejected_response] * 10 |
| |
| self.PatchObject(urllib, 'urlopen', autospec=True, |
| side_effect=return_value) |
| |
| def testTreeIsOpen(self): |
| """Tests that we return True is the tree is open.""" |
| self._SetupMockTreeStatusResponses(rejected_status_count=5, |
| retries_500=5) |
| self.assertTrue(tree_status.IsTreeOpen(status_url=self.status_url, |
| period=0)) |
| |
| def testTreeIsClosed(self): |
| """Tests that we return false is the tree is closed.""" |
| self._SetupMockTreeStatusResponses(output_final_status=False) |
| self.assertFalse(tree_status.IsTreeOpen(status_url=self.status_url, |
| period=0.1)) |
| |
| def testTreeIsThrottled(self): |
| """Tests that we return True if the tree is throttled.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is throttled (flaky bug on flaky builder)', |
| final_general_state=constants.TREE_THROTTLED) |
| self.assertTrue(tree_status.IsTreeOpen(status_url=self.status_url, |
| throttled_ok=True)) |
| |
| def testTreeIsThrottledNotOk(self): |
| """Tests that we respect throttled_ok""" |
| self._SetupMockTreeStatusResponses( |
| rejected_tree_status='Tree is throttled (flaky bug on flaky builder)', |
| rejected_general_state=constants.TREE_THROTTLED, |
| output_final_status=False) |
| self.assertFalse(tree_status.IsTreeOpen(status_url=self.status_url, |
| period=0.1)) |
| |
| def testWaitForStatusOpen(self): |
| """Tests that we can wait for a tree open response.""" |
| self._SetupMockTreeStatusResponses() |
| self.assertEqual(tree_status.WaitForTreeStatus(status_url=self.status_url), |
| constants.TREE_OPEN) |
| |
| |
| def testWaitForStatusThrottled(self): |
| """Tests that we can wait for a tree open response.""" |
| self._SetupMockTreeStatusResponses( |
| final_general_state=constants.TREE_THROTTLED) |
| self.assertEqual(tree_status.WaitForTreeStatus(status_url=self.status_url, |
| throttled_ok=True), |
| constants.TREE_THROTTLED) |
| |
| def testWaitForStatusFailure(self): |
| """Tests that we can wait for a tree open response.""" |
| self._SetupMockTreeStatusResponses(output_final_status=False) |
| self.assertRaises(timeout_util.TimeoutError, |
| tree_status.WaitForTreeStatus, |
| status_url=self.status_url, |
| period=0.1) |
| |
| def testGetStatusDictParsesMessage(self): |
| """Tests that _GetStatusDict parses message correctly.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is throttled (foo canary: taco investigating)', |
| final_general_state=constants.TREE_OPEN) |
| data = tree_status._GetStatusDict(self.status_url) |
| self.assertEqual(data[tree_status.TREE_STATUS_MESSAGE], |
| 'foo canary: taco investigating') |
| |
| def testGetStatusDictEmptyMessage(self): |
| """Tests that _GetStatusDict stores an empty string for unknown format.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is throttled. foo canary -> crbug.com/bar', |
| final_general_state=constants.TREE_OPEN) |
| data = tree_status._GetStatusDict(self.status_url) |
| self.assertEqual(data[tree_status.TREE_STATUS_MESSAGE], '') |
| |
| def testGetStatusDictRawMessage(self): |
| """Tests that _GetStatusDict stores raw message if requested.""" |
| self._SetupMockTreeStatusResponses(final_tree_status='Tree is open (taco).', |
| final_general_state=constants.TREE_OPEN) |
| data = tree_status._GetStatusDict(self.status_url, raw_message=True) |
| self.assertEqual(data[tree_status.TREE_STATUS_MESSAGE], |
| 'Tree is open (taco).') |
| |
| def testGetExperimentalBuilders(self): |
| """Tests that GetExperimentalBuilders parses out EXPERIMENTAL-BUILDERS=.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status=( |
| 'Tree is open (EXPERIMENTAL=amd64-generic-paladin).'), |
| final_general_state=constants.TREE_OPEN) |
| builders = tree_status.GetExperimentalBuilders(self.status_url) |
| self.assertItemsEqual(builders, ['amd64-generic-paladin']) |
| |
| self._SetupMockTreeStatusResponses( |
| final_tree_status=('Tree is open ' |
| '(EXPERIMENTAL=amd64-generic-paladin ' |
| 'EXPERIMENTAL=arm-generic-paladin).'), |
| final_general_state=constants.TREE_OPEN) |
| builders = tree_status.GetExperimentalBuilders(self.status_url) |
| self.assertItemsEqual( |
| builders, |
| ['amd64-generic-paladin', 'arm-generic-paladin']) |
| |
| # Builders not in the site config are filtered out. |
| self._SetupMockTreeStatusResponses( |
| final_tree_status=( |
| 'Tree is open (EXPERIMENTAL=foo-generic-paladin).'), |
| final_general_state=constants.TREE_OPEN) |
| builders = tree_status.GetExperimentalBuilders(self.status_url) |
| self.assertEqual(builders, []) |
| |
| # Case insensitive. |
| self._SetupMockTreeStatusResponses( |
| final_tree_status=( |
| 'Tree is open (experimental=amd64-generic-paladin).'), |
| final_general_state=constants.TREE_OPEN) |
| builders = tree_status.GetExperimentalBuilders(self.status_url) |
| self.assertItemsEqual(builders, ['amd64-generic-paladin']) |
| |
| def testGetExperimentalBuildersTimeout(self): |
| """Tests timeout behavior of GetExperimentalBuilders.""" |
| with mock.patch.object(tree_status, '_GetStatusDict') as m: |
| m.side_effect = lambda _: time.sleep(10) |
| with self.assertRaises(timeout_util.TimeoutError): |
| tree_status.GetExperimentalBuilders(self.status_url, timeout=1) |
| |
| def testUpdateTreeStatusWithEpilogue(self): |
| """Tests that epilogue is appended to the message.""" |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.UpdateTreeStatus( |
| constants.TREE_CLOSED, 'failure', announcer='foo', |
| epilogue='bar') |
| m.assert_called_once_with(mock.ANY, 'Tree is closed (foo: failure | bar)') |
| |
| def testUpdateTreeStatusWithoutEpilogue(self): |
| """Tests that the tree status message is created as expected.""" |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.UpdateTreeStatus( |
| constants.TREE_CLOSED, 'failure', announcer='foo') |
| m.assert_called_once_with(mock.ANY, 'Tree is closed (foo: failure)') |
| |
| def testUpdateTreeStatusUnknownStatus(self): |
| """Tests that the exception is raised on unknown tree status.""" |
| with mock.patch.object(tree_status, '_UpdateTreeStatus'): |
| self.assertRaises(tree_status.InvalidTreeStatus, |
| tree_status.UpdateTreeStatus, 'foostatus', 'failure') |
| |
| def testThrottlesTreeOnWithBuildNumberAndType(self): |
| """Tests that tree is throttled with the build number in the message.""" |
| self._SetupMockTreeStatusResponses(final_tree_status='Tree is open (taco)', |
| final_general_state=constants.TREE_OPEN) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure', buildnumber=1234, |
| internal=True) |
| m.assert_called_once_with(mock.ANY, |
| 'Tree is throttled (foo-i-1234: failure)') |
| |
| def testThrottlesTreeOnWithBuildNumberAndPublicType(self): |
| """Tests that tree is throttled with the build number in the message.""" |
| self._SetupMockTreeStatusResponses(final_tree_status='Tree is open (taco)', |
| final_general_state=constants.TREE_OPEN) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure', buildnumber=1234, |
| internal=False) |
| m.assert_called_once_with(mock.ANY, |
| 'Tree is throttled (foo-p-1234: failure)') |
| |
| def testThrottlesTreeOnOpen(self): |
| """Tests that ThrottleOrCloseTheTree throttles the tree if tree is open.""" |
| self._SetupMockTreeStatusResponses(final_tree_status='Tree is open (taco)', |
| final_general_state=constants.TREE_OPEN) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure') |
| m.assert_called_once_with(mock.ANY, 'Tree is throttled (foo: failure)') |
| |
| def testThrottlesTreeOnThrottled(self): |
| """Tests ThrottleOrCloseTheTree throttles the tree if tree is throttled.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is throttled (taco)', |
| final_general_state=constants.TREE_THROTTLED) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure') |
| # Also make sure that previous status message is included. |
| m.assert_called_once_with(mock.ANY, |
| 'Tree is throttled (foo: failure | taco)') |
| |
| def testClosesTheTreeOnClosed(self): |
| """Tests ThrottleOrCloseTheTree closes the tree if tree is closed.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is closed (taco)', |
| final_general_state=constants.TREE_CLOSED) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure') |
| m.assert_called_once_with(mock.ANY, |
| 'Tree is closed (foo: failure | taco)') |
| |
| def testClosesTheTreeOnMaintenance(self): |
| """Tests ThrottleOrCloseTheTree closes the tree if tree is closed.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is under maintenance (taco)', |
| final_general_state=constants.TREE_MAINTENANCE) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure') |
| m.assert_called_once_with( |
| mock.ANY, |
| 'Tree is under maintenance (foo: failure | taco)') |
| |
| def testDiscardUpdateFromTheSameAnnouncer(self): |
| """Tests we don't include messages from the same announcer.""" |
| self._SetupMockTreeStatusResponses( |
| final_tree_status='Tree is throttled (foo: failure | bar: taco)', |
| final_general_state=constants.TREE_THROTTLED) |
| with mock.patch.object(tree_status, '_UpdateTreeStatus') as m: |
| tree_status.ThrottleOrCloseTheTree('foo', 'failure') |
| # Also make sure that previous status message is included. |
| m.assert_called_once_with(mock.ANY, |
| 'Tree is throttled (foo: failure | bar: taco)') |
| |
| |
| class TestGettingSheriffEmails(cros_test_lib.MockTestCase): |
| """Tests functions related to retrieving the sheriff's email address.""" |
| |
| def testParsingSheriffEmails(self): |
| """Tests parsing the raw data to get sheriff emails.""" |
| # Test parsing when there is only one sheriff. |
| raw_line = "document.write('taco')" |
| self.PatchObject(tree_status, '_OpenSheriffURL', return_value=raw_line) |
| self.assertEqual(tree_status.GetSheriffEmailAddresses('chrome'), |
| ['taco@google.com']) |
| |
| # Test parsing when there are multiple sheriffs. |
| raw_line = "document.write('taco, burrito')" |
| self.PatchObject(tree_status, '_OpenSheriffURL', return_value=raw_line) |
| self.assertEqual(tree_status.GetSheriffEmailAddresses('chrome'), |
| ['taco@google.com', 'burrito@google.com']) |
| |
| # Test parsing when sheriff is None. |
| raw_line = "document.write('None (channel is sheriff)')" |
| self.PatchObject(tree_status, '_OpenSheriffURL', return_value=raw_line) |
| self.assertEqual(tree_status.GetSheriffEmailAddresses('chrome'), []) |