| # -*- coding: utf-8 -*- |
| # Copyright 2017 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. |
| |
| """Module containing the unit tests for handle_changes_stages.""" |
| |
| from __future__ import print_function |
| |
| import itertools |
| import mock |
| |
| from chromite.cbuildbot import relevant_changes |
| from chromite.cbuildbot.stages import handle_changes_stages |
| from chromite.cbuildbot.stages import generic_stages |
| from chromite.cbuildbot.stages import generic_stages_unittest |
| from chromite.cbuildbot.stages import sync_stages |
| from chromite.lib import builder_status_lib |
| from chromite.lib import cidb |
| from chromite.lib import clactions |
| from chromite.lib import config_lib |
| from chromite.lib import constants |
| from chromite.lib import hwtest_results |
| from chromite.lib import timeout_util |
| from chromite.lib import tree_status |
| from chromite.lib.buildstore import FakeBuildStore |
| |
| |
| # pylint: disable=protected-access |
| |
| |
| class CommitQueueHandleChangesStageTests( |
| generic_stages_unittest.AbstractStageTestCase): |
| """Tests for CommitQueueHandleChangesStag.""" |
| |
| BOT_ID = 'master-paladin' |
| |
| def setUp(self): |
| self._Prepare() |
| self.buildstore = FakeBuildStore() |
| self.mock_cidb = self.buildstore.GetCIDBHandle() |
| |
| self.partial_submit_changes = ['A', 'B'] |
| self.other_changes = ['C', 'D'] |
| self.changes = self.other_changes + self.partial_submit_changes |
| self.PatchObject(builder_status_lib, 'GetFailedMessages') |
| self.PatchObject(relevant_changes.RelevantChanges, |
| '_GetSlaveMappingAndCLActions', |
| return_value=(dict(), [])) |
| self.PatchObject(clactions, 'GetRelevantChangesForBuilds') |
| self.PatchObject(tree_status, 'WaitForTreeStatus', |
| return_value=constants.TREE_OPEN) |
| self.PatchObject(relevant_changes.RelevantChanges, |
| 'GetPreviouslyPassedSlavesForChanges') |
| self.mock_record_metrics = self.PatchObject( |
| handle_changes_stages.CommitQueueHandleChangesStage, |
| '_RecordSubmissionMetrics') |
| |
| self.sync_stage = self._MockSyncStage() |
| self.completion_stage = mock.Mock() |
| |
| def tearDown(self): |
| cidb.CIDBConnectionFactory.ClearMock() |
| |
| def _MockSyncStage(self, tree_was_open=True): |
| sync_stage = sync_stages.CommitQueueSyncStage(self._run, self.buildstore) |
| sync_stage.pool = mock.MagicMock() |
| sync_stage.pool.applied = self.changes |
| sync_stage.pool.tree_was_open = tree_was_open |
| |
| sync_stage.pool.handle_failure_mock = self.PatchObject( |
| sync_stage.pool, 'HandleValidationFailure') |
| sync_stage.pool.handle_timeout_mock = self.PatchObject( |
| sync_stage.pool, 'HandleValidationTimeout') |
| sync_stage.pool.submit_pool_mock = self.PatchObject( |
| sync_stage.pool, 'SubmitPool') |
| |
| return sync_stage |
| |
| # pylint: disable=arguments-differ |
| def ConstructStage(self, sync_stage=None, completion_stage=None): |
| sync_stage = sync_stage or self.sync_stage |
| completion_stage = completion_stage or self.completion_stage |
| return handle_changes_stages.CommitQueueHandleChangesStage( |
| self._run, self.buildstore, sync_stage, completion_stage) |
| |
| def test_GetBuildsPassedSyncStage(self): |
| """Test _GetBuildsPassedSyncStage.""" |
| stage = self.ConstructStage() |
| self.PatchObject(FakeBuildStore, 'GetBuildsStages', return_value=[ |
| {'build_config': 's_1', 'status': 'pass', 'name': 'CommitQueueSync'}, |
| {'build_config': 's_2', 'status': 'pass', 'name': 'CommitQueueSync'}, |
| {'build_config': 's_3', 'status': 'fail', 'name': 'CommitQueueSync'}, |
| {'build_config': 'master-paladin', 'status': 'pass', |
| 'name': 'CommitQueueSync'}]) |
| |
| builds = stage._GetBuildsPassedSyncStage( |
| 'build_id', ['id_1', 'id_2']) |
| self.assertItemsEqual(builds, ['s_1', 's_2', 'master-paladin']) |
| |
| def _MockPartialSubmit(self, stage): |
| self.PatchObject(relevant_changes.RelevantChanges, |
| 'GetRelevantChangesForSlaves', |
| return_value={'master-paladin': {mock.Mock()}}) |
| self.PatchObject(handle_changes_stages.CommitQueueHandleChangesStage, |
| '_GetBuildsPassedSyncStage') |
| stage.sync_stage.pool.SubmitPartialPool.return_value = self.changes |
| |
| def testHandleCommitQueueFailureWithOpenTree(self): |
| """Test _HandleCommitQueueFailure with open tree.""" |
| stage = self.ConstructStage() |
| self._MockPartialSubmit(stage) |
| self.PatchObject(tree_status, 'WaitForTreeStatus', |
| return_value=constants.TREE_OPEN) |
| self.PatchObject(generic_stages.BuilderStage, |
| 'GetScheduledSlaveBuildbucketIds', return_value=[]) |
| |
| stage._HandleCommitQueueFailure(set(['test1']), set(), set(), False) |
| stage.sync_stage.pool.handle_failure_mock.assert_called_once_with( |
| mock.ANY, sanity=True, no_stat=set(), changes=self.changes, |
| failed_hwtests=None) |
| |
| def testHandleCommitQueueFailureWithThrottledTree(self): |
| """Test _HandleCommitQueueFailure with throttled tree.""" |
| stage = self.ConstructStage() |
| self._MockPartialSubmit(stage) |
| self.PatchObject(tree_status, 'WaitForTreeStatus', |
| return_value=constants.TREE_THROTTLED) |
| self.PatchObject(generic_stages.BuilderStage, |
| 'GetScheduledSlaveBuildbucketIds', return_value=[]) |
| |
| stage._HandleCommitQueueFailure(set(['test1']), set(), set(), False) |
| stage.sync_stage.pool.handle_failure_mock.assert_called_once_with( |
| mock.ANY, sanity=False, no_stat=set(), changes=self.changes, |
| failed_hwtests=None) |
| |
| def testHandleCommitQueueFailureWithClosedTree(self): |
| """Test _HandleCommitQueueFailure with closed tree.""" |
| stage = self.ConstructStage() |
| self._MockPartialSubmit(stage) |
| self.PatchObject(tree_status, 'WaitForTreeStatus', |
| side_effect=timeout_util.TimeoutError()) |
| self.PatchObject(generic_stages.BuilderStage, |
| 'GetScheduledSlaveBuildbucketIds', return_value=[]) |
| |
| stage._HandleCommitQueueFailure(set(['test1']), set(), set(), False) |
| stage.sync_stage.pool.handle_failure_mock.assert_called_once_with( |
| mock.ANY, sanity=False, no_stat=set(), changes=self.changes, |
| failed_hwtests=None) |
| |
| def testHandleCommitQueueFailureWithFailedHWtests(self): |
| """Test _HandleCommitQueueFailure with failed HWtests.""" |
| stage = self.ConstructStage() |
| self._MockPartialSubmit(stage) |
| master_build_id = stage._run.attrs.metadata.GetValue('build_id') |
| db = self.mock_cidb |
| slave_build_id = db.InsertBuild( |
| 'slave_1', 1, 'slave_1', 'bot_hostname', |
| master_build_id=master_build_id, buildbucket_id='123') |
| cidb.CIDBConnectionFactory.SetupMockCidb(db) |
| mock_failed_hwtests = mock.Mock() |
| mock_get_hwtests = self.PatchObject( |
| hwtest_results.HWTestResultManager, |
| 'GetFailedHWTestsFromCIDB', return_value=mock_failed_hwtests) |
| self.PatchObject(tree_status, 'WaitForTreeStatus', |
| return_value=constants.TREE_OPEN) |
| self.PatchObject(generic_stages.BuilderStage, |
| 'GetScheduledSlaveBuildbucketIds', return_value=['123']) |
| |
| stage._HandleCommitQueueFailure(set(['test1']), set(), set(), False) |
| stage.sync_stage.pool.handle_failure_mock.assert_called_once_with( |
| mock.ANY, sanity=True, no_stat=set(), changes=self.changes, |
| failed_hwtests=mock_failed_hwtests) |
| mock_get_hwtests.assert_called_once_with(db, [slave_build_id]) |
| |
| def VerifyStage(self, failing, inflight, no_stat, handle_failure=False, |
| handle_timeout=False, sane_tot=True, stage=None, |
| all_slaves=None, slave_stages=None, fatal=True, |
| self_destructed=False): |
| """Runs and Verifies PerformStage. |
| |
| Args: |
| failing: The names of the builders that failed. |
| inflight: The names of the buiders that timed out. |
| no_stat: The names of the builders that had no status. |
| handle_failure: If True, calls HandleValidationFailure. |
| handle_timeout: If True, calls HandleValidationTimeout. |
| sane_tot: If not true, assumes TOT is not sane. |
| stage: If set, use this constructed stage, otherwise create own. |
| all_slaves: Optional set of all slave configs. |
| slave_stages: Optional list of slave stages. |
| fatal: Optional boolean indicating whether the completion_stage failed |
| with fatal. Default to True. |
| self_destructed: Optional boolean indicating whether the completion_stage |
| self_destructed. Default to False. |
| """ |
| if not stage: |
| stage = self.ConstructStage() |
| |
| stage._run.attrs.metadata.UpdateWithDict( |
| {constants.SELF_DESTRUCTED_BUILD: self_destructed}) |
| |
| # Setup the stage to look at the specified configs. |
| all_slaves = list(all_slaves or set(failing + inflight + no_stat)) |
| all_started_slaves = list(all_slaves or set(failing + inflight)) |
| configs = [config_lib.BuildConfig(name=x) for x in all_slaves] |
| self.PatchObject(stage, '_GetSlaveConfigs', return_value=configs) |
| |
| statuses = {} |
| for x in failing: |
| statuses[x] = builder_status_lib.BuilderStatus( |
| constants.BUILDER_STATUS_FAILED, message=None) |
| for x in inflight: |
| statuses[x] = builder_status_lib.BuilderStatus( |
| constants.BUILDER_STATUS_INFLIGHT, message=None) |
| for x in no_stat: |
| statuses[x] = builder_status_lib.BuilderStatus( |
| constants.BUILDER_STATUS_MISSING, message=None) |
| self.completion_stage.GetSlaveStatuses.return_value = statuses |
| self.completion_stage.GetFatal.return_value = fatal |
| |
| # Setup DB and provide list of slave stages. |
| mock_cidb = mock.MagicMock() |
| cidb.CIDBConnectionFactory.SetupMockCidb(mock_cidb) |
| if slave_stages is None: |
| slave_stages = [] |
| critical_stages = ( |
| relevant_changes.TriageRelevantChanges.STAGE_SYNC) |
| for stage_name, slave in itertools.product( |
| critical_stages, all_started_slaves): |
| slave_stages.append({'name': stage_name, |
| 'build_config': slave, |
| 'status': constants.BUILDER_STATUS_PASSED}) |
| self.PatchObject(self.buildstore, 'GetBuildStatuses', return_value=[]) |
| self.PatchObject(self.buildstore, 'GetBuildsStages', |
| return_value=slave_stages) |
| |
| # Set up SubmitPartialPool to provide a list of changes to look at. |
| self.PatchObject(stage.sync_stage.pool, 'SubmitPartialPool', |
| return_value=self.other_changes) |
| |
| # Actually run the stage. |
| stage.PerformStage() |
| |
| if fatal: |
| stage.sync_stage.pool.submit_pool_mock.assert_not_called() |
| self.mock_record_metrics.assert_called_once_with(False) |
| else: |
| stage.sync_stage.pool.submit_pool_mock.assert_called_once_with( |
| reason=constants.STRATEGY_CQ_SUCCESS) |
| self.mock_record_metrics.assert_called_once_with(True) |
| |
| if handle_failure: |
| stage.sync_stage.pool.handle_failure_mock.assert_called_once_with( |
| mock.ANY, no_stat=set(no_stat), sanity=sane_tot, |
| changes=self.other_changes, failed_hwtests=mock.ANY) |
| |
| if handle_timeout: |
| stage.sync_stage.pool.handle_timeout_mock.assert_called_once_with( |
| sanity=mock.ANY, changes=self.other_changes) |
| |
| def testCompletionSuccess(self): |
| """Verify stage when the completion_stage succeeded.""" |
| self.VerifyStage([], [], [], fatal=False) |
| |
| def testCompletionWithInflightSlaves(self): |
| """Verify stage when the completion_stage failed with inflight slaves.""" |
| self.VerifyStage([], ['foo'], [], handle_timeout=True) |
| |
| def testCompletionSelfDestructedWithInflightSlaves(self): |
| """Verify stage when the completion_stage self_destructed with inflight.""" |
| self.VerifyStage([], ['foo'], [], self_destructed=True, handle_failure=True) |
| |
| def testCompletionSelfDestructedWithFailingSlaves(self): |
| """Verify stage when the completion_stage self_destructed with failing.""" |
| self.VerifyStage(['foo'], [], [], self_destructed=True, handle_failure=True) |
| |
| def testCompletionSelfDestructedWithdNoStatSlaves(self): |
| """Verify stage when the completion_stage self_destructed with no_stat.""" |
| self.VerifyStage([], [], ['foo'], self_destructed=True, handle_failure=True) |