| # Copyright 2012 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unittests for report stages.""" |
| |
| import datetime as dt |
| import json |
| from unittest import mock |
| |
| import pytest # pylint: disable=import-error |
| |
| from chromite.cbuildbot import cbuildbot_alerts |
| from chromite.cbuildbot import cbuildbot_run |
| from chromite.cbuildbot import cbuildbot_unittest |
| from chromite.cbuildbot import commands |
| 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 import alerts |
| from chromite.lib import chromeos_version |
| from chromite.lib import cidb |
| from chromite.lib import config_lib |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import failure_message_lib_unittest |
| from chromite.lib import fake_cidb |
| from chromite.lib import gs_unittest |
| from chromite.lib import metrics |
| from chromite.lib import osutils |
| from chromite.lib import results_lib |
| from chromite.lib import retry_stats |
| from chromite.lib import toolchain |
| from chromite.lib.buildstore import FakeBuildStore |
| |
| |
| # 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() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| build_id = self.fake_db.InsertBuild( |
| "builder name", 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 = chromeos_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 ConstructStage(self): |
| return report_stages.BuildReexecutionFinishedStage( |
| self._run, self.buildstore |
| ) |
| |
| |
| class ConfigDumpStageTest(generic_stages_unittest.AbstractStageTestCase): |
| """Tests that ConfigDumpStage runs without syntax error.""" |
| |
| def setUp(self): |
| self.buildstore = FakeBuildStore() |
| |
| def ConstructStage(self): |
| return report_stages.ConfigDumpStage(self._run, self.buildstore) |
| |
| 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() |
| self.buildstore = FakeBuildStore(self.db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.db) |
| self._Prepare(build_id=1) |
| |
| # Our API here is not great when it comes to kwargs passing. |
| def _Prepare(self, **kwargs): # pylint: disable=arguments-differ |
| """Prepare stage with config['master']=True.""" |
| super()._Prepare(**kwargs) |
| self._run.config["master"] = True |
| |
| def ConstructStage(self): |
| return report_stages.SlaveFailureSummaryStage( |
| self._run, self.buildstore |
| ) |
| |
| 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, |
| 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.buildstore, "GetBuildsFailures", return_value=[fake_failure] |
| ) |
| self.PatchObject(cbuildbot_alerts, "PrintBuildbotLink") |
| self.RunStage() |
| self.assertEqual(cbuildbot_alerts.PrintBuildbotLink.call_count, 1) |
| |
| |
| @pytest.mark.usefixtures("singleton_manager") |
| class BuildStartStageTest(generic_stages_unittest.AbstractStageTestCase): |
| """Tests that BuildStartStage behaves as expected.""" |
| |
| def setUp(self): |
| self.db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.db) |
| retry_stats.SetupStats() |
| |
| master_build_id = self.db.InsertBuild( |
| "master_build", 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-full") |
| 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, self.buildstore) |
| |
| |
| 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") |
| |
| # 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() |
| self.buildstore = FakeBuildStore(self.mock_cidb) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.mock_cidb) |
| |
| # Setup topology for unittests |
| keyvals = {topology.DATASTORE_WRITER_CREDS_KEY: "./foo/bar.cert"} |
| topology_unittest.FakeFetchTopology(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, self.buildstore, None) |
| |
| |
| @pytest.mark.usefixtures("singleton_manager") |
| class ReportStageTest(AbstractReportStageTestCase): |
| """Test the Report stage.""" |
| |
| RELEASE_TAG = "" |
| |
| def setUp(self): |
| self.mock_cidb.GetSlaveStatuses = mock.Mock(return_value=None) |
| |
| def testCheckResults(self): |
| """Basic sanity check for results stage functionality""" |
| self.CreateMockOverlay("amd64-generic") |
| |
| stages = [ |
| { |
| "build_config": "build1", |
| "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, |
| }, |
| { |
| "build_config": "build1", |
| "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, |
| }, |
| { |
| "build_config": "build1", |
| "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.buildstore.GetBuildsStages = 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.assertEqual(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.assertEqual(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.assertEqual(calls, commands.UploadArchivedFile.call_args_list) |
| |
| # Disabled because of: http://b/227316467 |
| # def testEmailNotifyMasterBranch(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.buildstore.UpdateLuciNotifyProperties = mock.Mock() |
| # notification_config_1 = config_lib.NotificationConfig( |
| # 'test1@chromium.org', threshold=1) |
| # notification_config_2 = config_lib.NotificationConfig( |
| # 'test2@chromium.org', threshold=2) |
| # notification_config_3 = config_lib.NotificationConfig( |
| # 'test3@chromium.org', threshold=3, template='explicit_template') |
| # self._Prepare( |
| # extra_config={ |
| # 'notification_configs': [ |
| # notification_config_1, |
| # notification_config_2, |
| # notification_config_3, |
| # ] |
| # }, |
| # cmd_args=['-r', self.build_root, '--branch', 'master', self.BOT_ID]) |
| # self._SetupUpdateStreakCounter(counter_value=-2) |
| # self.RunStage() |
| # self.buildstore.UpdateLuciNotifyProperties.assert_called_once_with( |
| # email_notify=[ |
| # notification_config_1.email_notify, |
| # notification_config_2.email_notify, |
| # ]) |
| |
| def testEmailNotifyNonMasterBranch(self): |
| """Do not send out email alerts for non-master branches.""" |
| self.PatchObject( |
| cbuildbot_run._BuilderRunBase, |
| "InEmailReportingEnvironment", |
| return_value=True, |
| ) |
| self.PatchObject(cros_build_lib, "HostIsCIBuilder", return_value=True) |
| self.buildstore.UpdateLuciNotifyProperties = mock.Mock() |
| self._Prepare( |
| extra_config={ |
| "notification_configs": [ |
| config_lib.NotificationConfig( |
| "test1@chromium.org", threshold=1 |
| ) |
| ] |
| } |
| ) |
| self._SetupUpdateStreakCounter(counter_value=-1) |
| self.RunStage() |
| self.buildstore.UpdateLuciNotifyProperties.assert_not_called() |
| |
| 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.assertIn("builder-name", metadata_dict) |
| self.assertIn("bot-hostname", metadata_dict) |
| |
| 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.assertIn("builder_name", tags_dict) |
| self.assertIn("bot_hostname", tags_dict) |
| 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) |
| |
| |
| @pytest.mark.usefixtures("singleton_manager") |
| 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() |