| # -*- coding: utf-8 -*- |
| # Copyright (c) 2012 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. |
| |
| """Unittests for report stages.""" |
| |
| from __future__ import print_function |
| |
| import datetime as dt |
| import json |
| import mock |
| import os |
| |
| from chromite.cbuildbot import cbuildbot_run |
| from chromite.cbuildbot import cbuildbot_unittest |
| from chromite.cbuildbot import commands |
| from chromite.cbuildbot import manifest_version |
| from chromite.cbuildbot import topology |
| from chromite.cbuildbot import topology_unittest |
| from chromite.cbuildbot.stages import generic_stages_unittest |
| from chromite.cbuildbot.stages import report_stages |
| from chromite.lib.const import waterfall |
| from chromite.lib import alerts |
| from chromite.lib import cidb |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import fake_cidb |
| from chromite.lib import failures_lib |
| from chromite.lib import failure_message_lib_unittest |
| from chromite.lib import gs_unittest |
| from chromite.lib import metadata_lib |
| from chromite.lib import metrics |
| from chromite.lib import osutils |
| from chromite.lib import patch_unittest |
| from chromite.lib import results_lib |
| from chromite.lib import retry_stats |
| from chromite.lib import risk_report |
| from chromite.lib import toolchain |
| |
| |
| # pylint: disable=protected-access |
| # pylint: disable=too-many-ancestors |
| |
| |
| class BuildReexecutionStageTest(generic_stages_unittest.AbstractStageTestCase): |
| """Tests that BuildReexecutionFinishedStage behaves as expected.""" |
| def setUp(self): |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| build_id = self.fake_db.InsertBuild( |
| 'builder name', 'waterfall', 1, 'build config', 'bot hostname') |
| |
| self._Prepare(build_id=build_id) |
| |
| release_tag = '4815.0.0-rc1' |
| self._run.attrs.release_tag = '4815.0.0-rc1' |
| fake_versioninfo = manifest_version.VersionInfo(release_tag, '39') |
| self.gs_mock = self.StartPatcher(gs_unittest.GSContextMock()) |
| self.gs_mock.SetDefaultCmdResult() |
| self.PatchObject(cbuildbot_run._BuilderRunBase, 'GetVersionInfo', |
| return_value=fake_versioninfo) |
| self.PatchObject(toolchain, 'GetToolchainsForBoard') |
| self.PatchObject(toolchain, 'GetToolchainTupleForBoard', |
| return_value=['i686-pc-linux-gnu', 'arm-none-eabi']) |
| |
| def tearDown(self): |
| cidb.CIDBConnectionFactory.SetupMockCidb() |
| |
| def testPerformStage(self): |
| """Test that a normal runs completes without error.""" |
| self.RunStage() |
| tags = self._run.attrs.metadata.GetValue(constants.METADATA_TAGS) |
| self.assertEqual(tags['version_full'], 'R39-4815.0.0-rc1') |
| |
| def testMasterSlaveVersionMismatch(self): |
| """Test that master/slave version mismatch causes failure.""" |
| master_release_tag = '9999.0.0-rc1' |
| master_build_id = self.fake_db.InsertBuild( |
| 'master', waterfall.WATERFALL_INTERNAL, 2, 'master config', |
| 'master hostname') |
| master_metadata = metadata_lib.CBuildbotMetadata() |
| master_metadata.UpdateKeyDictWithDict( |
| 'version', {'full' : 'R39-9999.0.0-rc1', |
| 'milestone': '39', |
| 'platform': master_release_tag}) |
| self._run.attrs.metadata.UpdateWithDict( |
| {'master_build_id': master_build_id}) |
| self.fake_db.UpdateMetadata(master_build_id, master_metadata) |
| |
| stage = self.ConstructStage() |
| with self.assertRaises(failures_lib.StepFailure): |
| stage.Run() |
| |
| def ConstructStage(self): |
| return report_stages.BuildReexecutionFinishedStage(self._run) |
| |
| |
| class ConfigDumpStageTest(generic_stages_unittest.AbstractStageTestCase): |
| """Tests that ConfigDumpStage runs without syntax error.""" |
| |
| def ConstructStage(self): |
| return report_stages.ConfigDumpStage(self._run) |
| |
| def testPerformStage(self): |
| self._Prepare() |
| self.RunStage() |
| |
| |
| class SlaveFailureSummaryStageTest( |
| generic_stages_unittest.AbstractStageTestCase): |
| """Tests that SlaveFailureSummaryStage behaves as expected.""" |
| |
| def setUp(self): |
| self.db = mock.MagicMock() |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.db) |
| self._Prepare(build_id=1) |
| |
| def _Prepare(self, **kwargs): |
| """Prepare stage with config['master']=True.""" |
| super(SlaveFailureSummaryStageTest, self)._Prepare(**kwargs) |
| self._run.config['master'] = True |
| |
| def ConstructStage(self): |
| return report_stages.SlaveFailureSummaryStage(self._run) |
| |
| def testPerformStage(self): |
| """Tests that stage runs without syntax errors.""" |
| fake_failure = ( |
| failure_message_lib_unittest.StageFailureHelper.CreateStageFailure( |
| build_id=10, |
| build_stage_id=11, |
| waterfall=waterfall.WATERFALL_EXTERNAL, |
| builder_name='builder_name', |
| build_number=12, |
| build_config='build-config', |
| stage_name='FailingStage', |
| stage_status=constants.BUILDER_STATUS_FAILED, |
| build_status=constants.BUILDER_STATUS_FAILED)) |
| self.PatchObject(self.db, 'GetSlaveFailures', return_value=[fake_failure]) |
| self.PatchObject(logging, 'PrintBuildbotLink') |
| self.RunStage() |
| self.assertEqual(logging.PrintBuildbotLink.call_count, 1) |
| |
| |
| class BuildStartStageTest(generic_stages_unittest.AbstractStageTestCase): |
| """Tests that BuildStartStage behaves as expected.""" |
| |
| def setUp(self): |
| self.db = fake_cidb.FakeCIDBConnection() |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.db) |
| retry_stats.SetupStats() |
| os.environ['BUILDBOT_MASTERNAME'] = waterfall.WATERFALL_EXTERNAL |
| |
| master_build_id = self.db.InsertBuild( |
| 'master_build', waterfall.WATERFALL_EXTERNAL, 1, |
| 'master_build_config', 'bot_hostname') |
| |
| self.PatchObject(toolchain, 'GetToolchainsForBoard') |
| self.PatchObject(toolchain, 'GetArchForTarget', return_value='x86') |
| |
| self._Prepare(build_id=None, master_build_id=master_build_id) |
| |
| def testPerformStage(self): |
| """Test that a normal run of the stage does a database insert.""" |
| self.RunStage() |
| |
| build_id = self._run.attrs.metadata.GetValue('build_id') |
| self.assertGreater(build_id, 0) |
| self.assertEqual(self._run.attrs.metadata.GetValue('db_type'), |
| cidb.CONNECTION_TYPE_MOCK) |
| |
| def testSuiteSchedulingEqualsFalse(self): |
| """Test that a run of the stage makes suite_scheduling False.""" |
| # Test suite_scheduling for **-paladin |
| self._Prepare(bot_id='amd64-generic-paladin') |
| self.RunStage() |
| self.assertFalse(self._run.attrs.metadata.GetValue('suite_scheduling')) |
| |
| def testSuiteSchedulingEqualsTrue(self): |
| """Test that a run of the stage makes suite_scheduling True.""" |
| # Test suite_scheduling for **-release |
| self._Prepare(bot_id='eve-release') |
| self.RunStage() |
| self.assertTrue(self._run.attrs.metadata.GetValue('suite_scheduling')) |
| |
| def testHandleSkipWithInstanceChange(self): |
| """Test that HandleSkip disables cidb and dies when necessary.""" |
| # This test verifies that switching to a 'mock' database type once |
| # metadata already has an id in 'previous_db_type' will fail. |
| self._run.attrs.metadata.UpdateWithDict({'build_id': 31337, |
| 'db_type': 'previous_db_type'}) |
| stage = self.ConstructStage() |
| self.assertRaises(AssertionError, stage.HandleSkip) |
| self.assertEqual(cidb.CIDBConnectionFactory.GetCIDBConnectionType(), |
| cidb.CONNECTION_TYPE_INV) |
| # The above test has the side effect of invalidating CIDBConnectionFactory. |
| # Undo that side effect so other unit tests can run. |
| cidb.CIDBConnectionFactory.SetupMockCidb() |
| |
| def testHandleSkipWithNoDbType(self): |
| """Test that HandleSkip passes when db_type is missing.""" |
| self._run.attrs.metadata.UpdateWithDict({'build_id': 31337}) |
| stage = self.ConstructStage() |
| stage.HandleSkip() |
| |
| def testHandleSkipWithDbType(self): |
| """Test that HandleSkip passes when db_type is specified.""" |
| self._run.attrs.metadata.UpdateWithDict( |
| {'build_id': 31337, |
| 'db_type': cidb.CONNECTION_TYPE_MOCK}) |
| stage = self.ConstructStage() |
| stage.HandleSkip() |
| |
| def ConstructStage(self): |
| return report_stages.BuildStartStage(self._run) |
| |
| |
| class AbstractReportStageTestCase( |
| generic_stages_unittest.AbstractStageTestCase, |
| cbuildbot_unittest.SimpleBuilderTestCase): |
| """Base class for testing the Report stage.""" |
| |
| def setUp(self): |
| for cmd in ((osutils, 'WriteFile'), |
| (commands, 'UploadArchivedFile'), |
| (alerts, 'SendEmail')): |
| self.StartPatcher(mock.patch.object(*cmd, autospec=True)) |
| retry_stats.SetupStats() |
| |
| self.PatchObject(report_stages.ReportStage, '_GetBuildDuration', |
| return_value=1000) |
| self.PatchObject(toolchain, 'GetToolchainsForBoard') |
| self.PatchObject(toolchain, 'GetArchForTarget', return_value='x86') |
| |
| # We need to mock out the function in risk_report that calls the real |
| # CL-Scanner API to avoid relying on external dependencies in the test. |
| self.PatchObject(risk_report, '_GetCLRisks', return_value={'1234': 1.0}) |
| |
| # Set up a general purpose cidb mock. Tests with more specific |
| # mock requirements can replace this with a separate call to |
| # SetupMockCidb |
| self.mock_cidb = mock.MagicMock() |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.mock_cidb) |
| |
| # Setup topology for unittests |
| keyvals = {topology.DATASTORE_WRITER_CREDS_KEY:'./foo/bar.cert'} |
| topology_unittest.FakeFetchTopologyFromCIDB(keyvals=keyvals) |
| |
| self._Prepare() |
| |
| def _SetupUpdateStreakCounter(self, counter_value=-1): |
| self.PatchObject(report_stages.ReportStage, '_UpdateStreakCounter', |
| autospec=True, return_value=counter_value) |
| |
| def ConstructStage(self): |
| return report_stages.ReportStage(self._run, None) |
| |
| |
| class ReportStageTest(AbstractReportStageTestCase): |
| """Test the Report stage.""" |
| |
| RELEASE_TAG = '' |
| |
| def testCheckResults(self): |
| """Basic sanity check for results stage functionality""" |
| stages = [ |
| { |
| 'name': 'stage1', |
| 'start_time': dt.datetime.now() - dt.timedelta(0, 500), |
| 'finish_time': dt.datetime.now() - dt.timedelta(0, 300), |
| 'status': constants.BUILDER_STATUS_PASSED, |
| }, |
| { |
| 'name': 'stage2', |
| 'start_time': dt.datetime.now() - dt.timedelta(0, 500), |
| 'finish_time': dt.datetime.now() - dt.timedelta(0, 200), |
| 'status': constants.BUILDER_STATUS_PASSED, |
| }, |
| { |
| 'name': 'stage3', |
| 'start_time': dt.datetime.now() - dt.timedelta(0, 200), |
| 'finish_time': dt.datetime.now() - dt.timedelta(0, 100), |
| 'status': constants.BUILDER_STATUS_PASSED, |
| }, |
| ] |
| statuses = [ |
| { |
| 'build_config': 'build1', |
| 'build_number': '64', |
| 'start_time': dt.datetime.now() - dt.timedelta(0, 600), |
| 'finish_time': dt.datetime.now() - dt.timedelta(0, 330), |
| 'status': constants.BUILDER_STATUS_PASSED, |
| }, |
| { |
| 'build_config': 'build2', |
| 'build_number': '27', |
| 'start_time': dt.datetime.now() - dt.timedelta(0, 300), |
| 'finish_time': dt.datetime.now() - dt.timedelta(0, 100), |
| 'status': constants.BUILDER_STATUS_PASSED, |
| }, |
| { |
| 'build_config': 'build3', |
| 'build_number': '288282', |
| 'start_time': dt.datetime.now() - dt.timedelta(0, 400), |
| 'finish_time': dt.datetime.now() - dt.timedelta(0, 200), |
| 'status': constants.BUILDER_STATUS_PASSED, |
| }, |
| ] |
| self.mock_cidb.GetBuildStages = mock.Mock( |
| return_value=stages) |
| self.mock_cidb.GetSlaveStatuses = mock.Mock( |
| return_value=statuses) |
| self._SetupUpdateStreakCounter() |
| self.PatchObject(report_stages.ReportStage, '_LinkArtifacts') |
| self.RunStage() |
| filenames = ( |
| 'LATEST-%s' % self.TARGET_MANIFEST_BRANCH, |
| 'LATEST-%s' % self.VERSION, |
| ) |
| calls = [mock.call(mock.ANY, mock.ANY, 'metadata.json', False, |
| update_list=True, acl=mock.ANY)] |
| calls += [mock.call(mock.ANY, mock.ANY, 'timeline-stages.html', |
| debug=False, update_list=True, acl=mock.ANY)] |
| calls += [mock.call(mock.ANY, mock.ANY, 'timeline-slaves.html', |
| debug=False, update_list=True, acl=mock.ANY)] |
| calls += [mock.call(mock.ANY, mock.ANY, filename, False, |
| acl=mock.ANY) for filename in filenames] |
| |
| # Verify build stages timeline contains the stages that were mocked. |
| self.assertEquals(calls, commands.UploadArchivedFile.call_args_list) |
| timeline_content = osutils.WriteFile.call_args_list[2][0][1] |
| for s in stages: |
| self.assertIn('["%s", new Date' % s['name'], timeline_content) |
| |
| # Verify slaves timeline contains the slaves that were mocked. |
| self.assertEquals(calls, commands.UploadArchivedFile.call_args_list) |
| timeline_content = osutils.WriteFile.call_args_list[3][0][1] |
| for s in statuses: |
| self.assertIn('["%s - %s", new Date' % |
| (s['build_config'], s['build_number']), timeline_content) |
| |
| def testDoNotUpdateLATESTMarkersWhenBuildFailed(self): |
| """Check that we do not update the latest markers on failed build.""" |
| self._SetupUpdateStreakCounter() |
| self.PatchObject(report_stages.ReportStage, '_LinkArtifacts') |
| self.PatchObject(results_lib.Results, 'BuildSucceededSoFar', |
| return_value=False) |
| stage = self.ConstructStage() |
| self.PatchObject(stage, 'GetBuildFailureMessage') |
| stage.Run() |
| calls = [mock.call(mock.ANY, mock.ANY, 'metadata.json', False, |
| update_list=True, acl=mock.ANY)] |
| calls += [mock.call(mock.ANY, mock.ANY, 'timeline-stages.html', |
| debug=False, update_list=True, acl=mock.ANY)] |
| self.assertEquals(calls, commands.UploadArchivedFile.call_args_list) |
| |
| def testAlertEmail(self): |
| """Send out alerts when streak counter reaches the threshold.""" |
| self.PatchObject(cbuildbot_run._BuilderRunBase, |
| 'InEmailReportingEnvironment', return_value=True) |
| self.PatchObject(cros_build_lib, 'HostIsCIBuilder', return_value=True) |
| self._Prepare(extra_config={'health_threshold': 3, |
| 'health_alert_recipients': ['foo@bar.org']}) |
| self._SetupUpdateStreakCounter(counter_value=-3) |
| self.RunStage() |
| # The mocking logic gets confused with SendEmail. |
| # pylint: disable=no-member |
| self.assertGreater(alerts.SendEmail.call_count, 0, |
| 'CQ health alerts emails were not sent.') |
| |
| def testAlertEmailOnFailingStreak(self): |
| """Continue sending out alerts when streak counter exceeds the threshold.""" |
| self.PatchObject(cbuildbot_run._BuilderRunBase, |
| 'InEmailReportingEnvironment', return_value=True) |
| self.PatchObject(cros_build_lib, 'HostIsCIBuilder', return_value=True) |
| self._Prepare(extra_config={'health_threshold': 3, |
| 'health_alert_recipients': ['foo@bar.org']}) |
| self._SetupUpdateStreakCounter(counter_value=-5) |
| self.RunStage() |
| # The mocking logic gets confused with SendEmail. |
| # pylint: disable=no-member |
| self.assertGreater(alerts.SendEmail.call_count, 0, |
| 'CQ health alerts emails were not sent.') |
| |
| def testWriteBasicMetadata(self): |
| """Test that WriteBasicMetadata writes expected keys correctly.""" |
| report_stages.WriteBasicMetadata(self._run) |
| metadata_dict = self._run.attrs.metadata.GetDict() |
| self.assertEqual(metadata_dict['build-number'], |
| generic_stages_unittest.DEFAULT_BUILD_NUMBER) |
| self.assertTrue(metadata_dict.has_key('builder-name')) |
| self.assertTrue(metadata_dict.has_key('bot-hostname')) |
| |
| def testWriteTagMetadata(self): |
| """Test that WriteTagMetadata writes expected keys correctly.""" |
| self.PatchObject(cros_build_lib, 'GetHostName', return_value='cros-wimpy2') |
| self._SetupUpdateStreakCounter() |
| report_stages.WriteTagMetadata(self._run) |
| tags_dict = self._run.attrs.metadata.GetValue(constants.METADATA_TAGS) |
| self.assertEqual(tags_dict['build_number'], |
| generic_stages_unittest.DEFAULT_BUILD_NUMBER) |
| self.assertTrue(tags_dict.has_key('builder_name')) |
| self.assertTrue(tags_dict.has_key('bot_hostname')) |
| self.RunStage() |
| tags_content = osutils.WriteFile.call_args_list[1][0][1] |
| tags_content_dict = json.loads(tags_content) |
| self.assertEqual(tags_content_dict['build_number'], |
| generic_stages_unittest.DEFAULT_BUILD_NUMBER) |
| |
| def testGetChildConfigsMetadataList(self): |
| """Test that GetChildConfigListMetadata generates child config metadata.""" |
| child_configs = [{'name': 'config1', 'boards': ['board1']}, |
| {'name': 'config2', 'boards': ['board2']}] |
| config_status_map = {'config1': True, |
| 'config2': False} |
| expected = [{'name': 'config1', 'boards': ['board1'], |
| 'status': constants.BUILDER_STATUS_PASSED}, |
| {'name': 'config2', 'boards': ['board2'], |
| 'status': constants.BUILDER_STATUS_FAILED}] |
| child_config_list = report_stages.GetChildConfigListMetadata( |
| child_configs, config_status_map) |
| self.assertEqual(expected, child_config_list) |
| |
| def testPerformStage(self): |
| """Test PerformStage.""" |
| mock_sd = self.PatchObject(metrics, 'CumulativeSecondsDistribution') |
| self.PatchObject(report_stages.ReportStage, 'ArchiveResults') |
| stage = self.ConstructStage() |
| stage.PerformStage() |
| self.assertEqual(mock_sd.call_count, 1) |
| |
| |
| class ReportStageForMasterCQTest(AbstractReportStageTestCase): |
| """Test the Report stage for master-paladin.""" |
| |
| RELEASE_TAG = '' |
| BOT_ID = 'master-paladin' |
| |
| def testPerformStage(self): |
| """Test PerformStage.""" |
| mock_sd = self.PatchObject(metrics, 'CumulativeSecondsDistribution') |
| self.PatchObject(report_stages.ReportStage, 'ArchiveResults') |
| stage = self.ConstructStage() |
| stage.PerformStage() |
| self.assertEqual(mock_sd.call_count, 2) |
| |
| |
| class ReportStageNoSyncTest(AbstractReportStageTestCase): |
| """Test the Report stage if SyncStage didn't complete. |
| |
| If SyncStage doesn't complete, we don't know the release tag, and can't |
| archive results. |
| """ |
| RELEASE_TAG = None |
| |
| def testCommitQueueResults(self): |
| """Check that we can run with a RELEASE_TAG of None.""" |
| self._SetupUpdateStreakCounter() |
| self.RunStage() |
| |
| |
| class DetectRelevantChangesStageTest( |
| generic_stages_unittest.AbstractStageTestCase): |
| """Test the DetectRelevantChangesStage.""" |
| |
| def setUp(self): |
| self._patch_factory = patch_unittest.MockPatchFactory() |
| self.changes = self._patch_factory.GetPatches(how_many=2) |
| |
| self._Prepare() |
| |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| build_id = self.fake_db.InsertBuild( |
| 'test-paladin', 'chromeos', 1, 'test-paladin', 'bot_hostname') |
| self._run.attrs.metadata.UpdateWithDict({'build_id': build_id}) |
| |
| def ConstructStage(self): |
| return report_stages.DetectRelevantChangesStage(self._run, |
| self._current_board, |
| self.changes) |
| |
| def testRecordActionForChangesWithIrrelevantAction(self): |
| """Test _RecordActionForChanges with irrelevant action..""" |
| stage = self.ConstructStage() |
| stage._RecordActionForChanges( |
| self.changes, constants.CL_ACTION_IRRELEVANT_TO_SLAVE) |
| action_history = self.fake_db.GetActionHistory() |
| self.assertEqual(len(action_history), 2) |
| for action in action_history: |
| self.assertEqual(action.action, constants.CL_ACTION_IRRELEVANT_TO_SLAVE) |
| |
| def testRecordActionForChangesWithEmptySet(self): |
| """Test _RecordActionForChanges with an empty changes set.""" |
| stage = self.ConstructStage() |
| stage._RecordActionForChanges( |
| set(), constants.CL_ACTION_IRRELEVANT_TO_SLAVE) |
| action_history = self.fake_db.GetActionHistory() |
| self.assertEqual(len(action_history), 0) |
| |
| def testRecordActionForChangesWithRelevantAction(self): |
| """Test _RecordActionForChanges with relevant action.""" |
| stage = self.ConstructStage() |
| stage._RecordActionForChanges( |
| self.changes, constants.CL_ACTION_RELEVANT_TO_SLAVE) |
| action_history = self.fake_db.GetActionHistory() |
| self.assertEqual(len(action_history), 2) |
| for action in action_history: |
| self.assertEqual(action.action, constants.CL_ACTION_RELEVANT_TO_SLAVE) |