| # 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 the artifact stages.""" |
| |
| import os |
| import sys |
| from unittest import mock |
| |
| from chromite.cbuildbot import cbuildbot_unittest |
| from chromite.cbuildbot import commands |
| from chromite.cbuildbot.stages import artifact_stages |
| from chromite.cbuildbot.stages import build_stages_unittest |
| from chromite.cbuildbot.stages import generic_stages_unittest |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_test_lib |
| from chromite.lib import failures_lib |
| from chromite.lib import osutils |
| from chromite.lib import parallel |
| from chromite.lib import parallel_unittest |
| from chromite.lib import path_util |
| from chromite.lib import results_lib |
| from chromite.lib.buildstore import FakeBuildStore |
| |
| |
| # pylint: disable=too-many-ancestors |
| |
| |
| class ArchiveStageTest( |
| generic_stages_unittest.AbstractStageTestCase, |
| cbuildbot_unittest.SimpleBuilderTestCase, |
| ): |
| """Exercise ArchiveStage functionality.""" |
| |
| # pylint: disable=protected-access |
| |
| RELEASE_TAG = "" |
| VERSION = "3333.1.0" |
| |
| def _PatchDependencies(self) -> None: |
| """Patch dependencies of ArchiveStage.PerformStage().""" |
| to_patch = [ |
| (parallel, "RunParallelSteps"), |
| (commands, "PushImages"), |
| (commands, "UploadArchivedFile"), |
| ] |
| self.AutoPatch(to_patch) |
| |
| def setUp(self) -> None: |
| self._PatchDependencies() |
| |
| self._Prepare() |
| self.buildstore = FakeBuildStore() |
| |
| # Our API here is not great when it comes to kwargs passing. |
| def _Prepare( |
| self, bot_id=None, **kwargs |
| ) -> None: # pylint: disable=arguments-differ |
| extra_config = {"upload_symbols": True, "push_image": True} |
| super()._Prepare(bot_id, extra_config=extra_config, **kwargs) |
| |
| def ConstructStage(self): |
| self._run.GetArchive().SetupArchivePath() |
| return artifact_stages.ArchiveStage( |
| self._run, self.buildstore, self._current_board |
| ) |
| |
| def testArchive(self) -> None: |
| """Simple did-it-run test.""" |
| # TODO(davidjames): Test the individual archive steps as well. |
| self.RunStage() |
| |
| # TODO(build): This test is not actually testing anything real. It confirms |
| # that PushImages is not called, but the mock for RunParallelSteps already |
| # prevents PushImages from being called, regardless of whether this is a |
| # trybot flow. |
| def testNoPushImagesForRemoteTrybot(self) -> None: |
| """Test that remote trybot overrides work to disable push images.""" |
| self._Prepare( |
| cmd_args=[ |
| "--remote-trybot", |
| "-r", |
| self.build_root, |
| "--buildnumber=1234", |
| "eve-release", |
| ] |
| ) |
| self.RunStage() |
| # pylint: disable=no-member |
| self.assertEqual(commands.PushImages.call_count, 0) |
| |
| def ConstructStageForArchiveStep(self): |
| """Stage construction for archive steps.""" |
| stage = self.ConstructStage() |
| self.PatchObject(stage._upload_queue, "put", autospec=True) |
| self.PatchObject( |
| path_util, "ToChrootPath", return_value="", autospec=True |
| ) |
| return stage |
| |
| |
| class UploadPrebuiltsStageTest( |
| generic_stages_unittest.RunCommandAbstractStageTestCase, |
| cbuildbot_unittest.SimpleBuilderTestCase, |
| ): |
| """Tests for the UploadPrebuilts stage.""" |
| |
| cmd = "upload_prebuilts" |
| RELEASE_TAG = "" |
| |
| # Our API here is not great when it comes to kwargs passing. |
| def _Prepare( |
| self, bot_id=None, **kwargs |
| ) -> None: # pylint: disable=arguments-differ |
| super()._Prepare(bot_id, **kwargs) |
| self.cmd = os.path.join( |
| self.build_root, constants.CHROMITE_BIN_SUBDIR, "upload_prebuilts" |
| ) |
| self._run.options.prebuilts = True |
| self.buildstore = FakeBuildStore() |
| |
| def ConstructStage(self): |
| return artifact_stages.UploadPrebuiltsStage( |
| self._run, self.buildstore, self._run.config.boards[-1] |
| ) |
| |
| def _VerifyBoardMap( |
| self, bot_id, count, board_map, public_args=None, private_args=None |
| ) -> None: |
| """Verify that the prebuilts are uploaded for the specified bot. |
| |
| Args: |
| bot_id: Bot to upload prebuilts for. |
| count: Number of assert checks that should be performed. |
| board_map: Map from slave boards to whether the bot is public. |
| public_args: List of extra arguments for public boards. |
| private_args: List of extra arguments for private boards. |
| """ |
| self._Prepare(bot_id) |
| self.RunStage() |
| public_prefix = [self.cmd] + (public_args or []) |
| private_prefix = [self.cmd] + (private_args or []) |
| for board, public in board_map.items(): |
| if public or public_args: |
| public_cmd = public_prefix + ["--slave-board", board] |
| self.assertCommandContains(public_cmd, expected=public) |
| count -= 1 |
| private_cmd = private_prefix + ["--slave-board", board, "--private"] |
| self.assertCommandContains(private_cmd, expected=not public) |
| count -= 1 |
| if board_map: |
| self.assertCommandContains( |
| [self.cmd, "--set-version", self._run.GetVersion()], |
| ) |
| count -= 1 |
| self.assertEqual( |
| count, |
| 0, |
| "Number of asserts performed does not match (%d remaining)" % count, |
| ) |
| |
| def testFullPrebuiltsUpload(self) -> None: |
| """Test uploading of full builder prebuilts.""" |
| self._VerifyBoardMap("amd64-generic-full", 0, {}) |
| self.assertCommandContains([self.cmd, "--git-sync"]) |
| |
| def testIncorrectCount(self) -> None: |
| """Test that _VerifyBoardMap asserts when the count is wrong.""" |
| self.assertRaises( |
| AssertionError, self._VerifyBoardMap, "amd64-generic-full", 1, {} |
| ) |
| |
| |
| class DebugSymbolsStageTest( |
| generic_stages_unittest.AbstractStageTestCase, |
| cbuildbot_unittest.SimpleBuilderTestCase, |
| ): |
| """Test DebugSymbolsStage""" |
| |
| # pylint: disable=protected-access |
| |
| def setUp(self) -> None: |
| self.CreateMockOverlay("amd64-generic") |
| self.StartPatcher(generic_stages_unittest.ArchivingStageMixinMock()) |
| |
| self.gen_mock = self.PatchObject(commands, "GenerateBreakpadSymbols") |
| self.gen_android_mock = self.PatchObject( |
| commands, "GenerateAndroidBreakpadSymbols" |
| ) |
| self.upload_mock = self.PatchObject(commands, "UploadSymbols") |
| self.upload_artifact_mock = self.PatchObject( |
| artifact_stages.DebugSymbolsStage, "UploadArtifact" |
| ) |
| self.tar_mock = self.PatchObject(commands, "GenerateDebugTarball") |
| |
| self.rc_mock = self.StartPatcher(cros_test_lib.RunCommandMock()) |
| self.rc_mock.SetDefaultCmdResult(stdout="") |
| |
| self.stage = None |
| |
| # Our API here is not great when it comes to kwargs passing. |
| # pylint: disable=arguments-differ |
| def _Prepare(self, extra_config=None, **kwargs) -> None: |
| """Prepare this stage for testing.""" |
| if extra_config is None: |
| extra_config = { |
| "archive_build_debug": True, |
| "upload_symbols": True, |
| } |
| super()._Prepare(extra_config=extra_config, **kwargs) |
| self._run.attrs.release_tag = self.VERSION |
| self.buildstore = FakeBuildStore() |
| |
| # pylint: enable=arguments-differ |
| |
| def ConstructStage(self): |
| """Create a DebugSymbolsStage instance for testing""" |
| self._run.GetArchive().SetupArchivePath() |
| return artifact_stages.DebugSymbolsStage( |
| self._run, self.buildstore, self._current_board |
| ) |
| |
| def assertBoardAttrEqual(self, attr, expected_value) -> None: |
| """Assert the value of a board run |attr| against |expected_value|.""" |
| value = self.stage.board_runattrs.GetParallel(attr) |
| self.assertEqual(expected_value, value) |
| |
| def _TestPerformStage( |
| self, extra_config=None, create_android_symbols_archive=False |
| ): |
| """Run PerformStage for the stage with the given extra config.""" |
| self._Prepare(extra_config=extra_config) |
| |
| self.tar_mock.side_effect = "/my/tar/ball" |
| self.stage = self.ConstructStage() |
| |
| if create_android_symbols_archive: |
| symbols_file = os.path.join( |
| self.stage.archive_path, constants.ANDROID_SYMBOLS_FILE |
| ) |
| osutils.Touch(symbols_file) |
| |
| try: |
| self.stage.PerformStage() |
| except Exception: |
| return self.stage._HandleStageException(sys.exc_info()) |
| |
| def testPerformStageWithSymbols(self) -> None: |
| """Smoke test for an PerformStage when debugging is enabled""" |
| self._TestPerformStage() |
| |
| self.assertEqual(self.gen_mock.call_count, 1) |
| self.assertEqual(self.gen_android_mock.call_count, 0) |
| self.assertEqual(self.upload_mock.call_count, 1) |
| self.assertEqual(self.tar_mock.call_count, 2) |
| self.assertEqual(self.upload_artifact_mock.call_count, 2) |
| |
| self.assertBoardAttrEqual("breakpad_symbols_generated", True) |
| self.assertBoardAttrEqual("debug_tarball_generated", True) |
| |
| def testPerformStageWithAndroidSymbols(self) -> None: |
| """Smoke test for an PerformStage when Android symbols are available""" |
| self._TestPerformStage(create_android_symbols_archive=True) |
| |
| self.assertEqual(self.gen_mock.call_count, 1) |
| self.assertEqual(self.gen_android_mock.call_count, 1) |
| self.assertEqual(self.upload_mock.call_count, 1) |
| self.assertEqual(self.tar_mock.call_count, 2) |
| self.assertEqual(self.upload_artifact_mock.call_count, 2) |
| |
| self.assertBoardAttrEqual("breakpad_symbols_generated", True) |
| self.assertBoardAttrEqual("debug_tarball_generated", True) |
| |
| def testPerformStageNoSymbols(self) -> None: |
| """Smoke test for an PerformStage when debugging is disabled""" |
| extra_config = { |
| "archive_build_debug": False, |
| "upload_symbols": False, |
| } |
| result = self._TestPerformStage(extra_config) |
| self.assertIsNone(result) |
| |
| self.assertEqual(self.gen_mock.call_count, 1) |
| self.assertEqual(self.gen_android_mock.call_count, 0) |
| self.assertEqual(self.upload_mock.call_count, 0) |
| self.assertEqual(self.tar_mock.call_count, 2) |
| self.assertEqual(self.upload_artifact_mock.call_count, 2) |
| |
| self.assertBoardAttrEqual("breakpad_symbols_generated", True) |
| self.assertBoardAttrEqual("debug_tarball_generated", True) |
| |
| def testGenerateCrashStillNotifies(self) -> None: |
| """Crashes in symbol generation should still notify external events.""" |
| |
| class TestError(Exception): |
| """Unique test exception""" |
| |
| self.gen_mock.side_effect = TestError("mew") |
| result = self._TestPerformStage() |
| self.assertIsInstance(result[0], failures_lib.InfrastructureFailure) |
| |
| self.assertEqual(self.gen_mock.call_count, 1) |
| self.assertEqual(self.gen_android_mock.call_count, 0) |
| self.assertEqual(self.upload_mock.call_count, 0) |
| self.assertEqual(self.tar_mock.call_count, 0) |
| self.assertEqual(self.upload_artifact_mock.call_count, 0) |
| |
| self.assertBoardAttrEqual("breakpad_symbols_generated", False) |
| self.assertBoardAttrEqual("debug_tarball_generated", False) |
| |
| def testUploadCrashStillNotifies(self) -> None: |
| """Crashes in symbol upload should still notify external events.""" |
| self.upload_mock.side_effect = failures_lib.BuildScriptFailure( |
| cros_build_lib.RunCommandError("mew"), "mew" |
| ) |
| result = self._TestPerformStage() |
| self.assertIs(result[0], results_lib.Results.FORGIVEN) |
| |
| self.assertBoardAttrEqual("breakpad_symbols_generated", True) |
| self.assertBoardAttrEqual("debug_tarball_generated", True) |
| |
| def testUploadCrashUploadsList(self) -> None: |
| """A crash in symbol upload should still post the failed list file.""" |
| self.upload_mock.side_effect = failures_lib.BuildScriptFailure( |
| cros_build_lib.RunCommandError("mew"), "mew" |
| ) |
| self._Prepare() |
| stage = self.ConstructStage() |
| |
| with mock.patch.object( |
| os.path, "exists" |
| ) as mock_exists, mock.patch.object( |
| artifact_stages.DebugSymbolsStage, "UploadArtifact" |
| ) as mock_upload: |
| mock_exists.return_value = True |
| self.assertRaises( |
| artifact_stages.DebugSymbolsUploadException, |
| stage.UploadSymbols, |
| stage._build_root, |
| stage._current_board, |
| ) |
| self.assertEqual(mock_exists.call_count, 1) |
| self.assertEqual(mock_upload.call_count, 1) |
| |
| |
| class UploadTestArtifactsStageMock( |
| generic_stages_unittest.ArchivingStageMixinMock |
| ): |
| """Partial mock for BuildImageStage.""" |
| |
| TARGET = ( |
| "chromite.cbuildbot.stages.artifact_stages.UploadTestArtifactsStage" |
| ) |
| ATTRS = generic_stages_unittest.ArchivingStageMixinMock.ATTRS + ( |
| "BuildAutotestTarballs", |
| "BuildTastTarball", |
| ) |
| |
| def BuildAutotestTarballs(self, *args, **kwargs) -> None: |
| with mock.patch.object( |
| commands, "BuildTarball", autospec=True |
| ), mock.patch.object( |
| commands, |
| "FindFilesWithPattern", |
| autospec=True, |
| return_value=["foo.txt"], |
| ): |
| self.backup["BuildAutotestTarballs"](*args, **kwargs) |
| |
| def BuildTastTarball(self, *args, **kwargs) -> None: |
| with mock.patch.object(commands, "BuildTarball", autospec=True): |
| self.backup["BuildTastTarball"](*args, **kwargs) |
| |
| |
| class UploadTestArtifactsStageTest( |
| build_stages_unittest.AllConfigsTestCase, |
| cbuildbot_unittest.SimpleBuilderTestCase, |
| ): |
| """Tests UploadTestArtifactsStage.""" |
| |
| def setUp(self) -> None: |
| self._release_tag = None |
| |
| osutils.SafeMakedirs(os.path.join(self.build_root, "chroot", "tmp")) |
| self.StartPatcher(UploadTestArtifactsStageMock()) |
| self.buildstore = FakeBuildStore() |
| |
| def ConstructStage(self): |
| return artifact_stages.UploadTestArtifactsStage( |
| self._run, self.buildstore, self._current_board |
| ) |
| |
| def RunTestsWithBotId(self, bot_id, options_tests=True) -> None: |
| """Test with the config for the specified bot_id.""" |
| self._Prepare(bot_id) |
| self._run.options.tests = options_tests |
| self._run.attrs.release_tag = "0.0.1" |
| |
| # Simulate images being ready. |
| board_runattrs = self._run.GetBoardRunAttrs(self._current_board) |
| board_runattrs.SetParallel("images_generated", True) |
| |
| generate_update_payloads_mock = self.PatchObject( |
| commands, "GeneratePayloads" |
| ) |
| |
| with parallel_unittest.ParallelMock(): |
| with self.RunStageWithConfig(): |
| if ( |
| self._run.config.upload_hw_test_artifacts |
| and self._run.config.images |
| ): |
| self.assertNotEqual( |
| generate_update_payloads_mock.call_count, 0 |
| ) |
| else: |
| self.assertEqual( |
| generate_update_payloads_mock.call_count, 0 |
| ) |
| |
| def testAllConfigs(self) -> None: |
| """Test all major configurations""" |
| self.RunAllConfigs(self.RunTestsWithBotId) |