| # 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 build stages.""" |
| |
| import contextlib |
| import os |
| from pathlib import Path |
| import tempfile |
| from unittest import mock |
| |
| from chromite.third_party.infra_libs.buildbucket.proto import ( |
| build_pb2, |
| builds_service_pb2, |
| ) |
| |
| from chromite.cbuildbot import cbuildbot_unittest |
| from chromite.cbuildbot import commands |
| from chromite.cbuildbot.stages import build_stages |
| from chromite.cbuildbot.stages import generic_stages_unittest |
| from chromite.cbuildbot.stages.generic_stages_unittest import patch |
| from chromite.cbuildbot.stages.generic_stages_unittest import patches |
| from chromite.lib import build_summary |
| from chromite.lib import buildbucket_v2 |
| 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 cros_sdk_lib |
| from chromite.lib import cros_test_lib |
| from chromite.lib import fake_cidb |
| from chromite.lib import osutils |
| from chromite.lib import parallel |
| from chromite.lib import parallel_unittest |
| from chromite.lib import partial_mock |
| from chromite.lib import path_util |
| from chromite.lib.buildstore import FakeBuildStore |
| |
| |
| # pylint: disable=too-many-ancestors |
| # pylint: disable=protected-access |
| |
| |
| class _RunAbstractStageTestCase( |
| generic_stages_unittest.RunCommandAbstractStageTestCase |
| ): |
| """Helper with a RunStage wrapper.""" |
| |
| def _Run(self, dir_exists): |
| """Helper for running the build.""" |
| with patch(os.path, "isdir", return_value=dir_exists): |
| self.RunStage() |
| |
| def ConstructStage(self): |
| """Returns an instance of the stage to be tested. |
| |
| Note: Must be implemented in subclasses. |
| """ |
| raise NotImplementedError( |
| self, "ConstructStage: Implement in your test" |
| ) |
| |
| |
| class InitSDKTest(_RunAbstractStageTestCase): |
| """Test building the SDK""" |
| |
| def setUp(self): |
| self.PatchObject(cros_sdk_lib, "GetChrootVersion", return_value="12") |
| self.cros_sdk = os.path.join( |
| self.tempdir, "buildroot", constants.CHROMITE_BIN_SUBDIR, "cros_sdk" |
| ) |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| def ConstructStage(self): |
| return build_stages.InitSDKStage(self._run, self.buildstore) |
| |
| def testFullBuildWithExistingChroot(self): |
| """Tests whether we create chroots for full builds.""" |
| self._PrepareFull() |
| self._Run(dir_exists=True) |
| self.assertCommandContains([self.cros_sdk]) |
| |
| def testBinBuildWithMissingChroot(self): |
| """Tests whether we create chroots when needed.""" |
| self._PrepareBin() |
| # Do not force chroot replacement in build config. |
| self._run._config.chroot_replace = False |
| self._Run(dir_exists=False) |
| self.assertCommandContains([self.cros_sdk]) |
| |
| def testFullBuildWithMissingChroot(self): |
| """Tests whether we create chroots when needed.""" |
| self._PrepareFull() |
| self._Run(dir_exists=True) |
| self.assertCommandContains([self.cros_sdk]) |
| |
| def testFullBuildWithNoSDK(self): |
| """Tests whether the --nosdk option works.""" |
| self._PrepareFull(extra_cmd_args=["--nosdk"]) |
| self._Run(dir_exists=False) |
| self.assertCommandContains([self.cros_sdk, "--bootstrap"]) |
| |
| def testBinBuildWithExistingChroot(self): |
| """Tests whether the --nosdk option works.""" |
| self._PrepareFull(extra_cmd_args=["--nosdk"]) |
| # Do not force chroot replacement in build config. |
| self._run._config.chroot_replace = False |
| self._run._config.separate_debug_symbols = False |
| self._run.config.useflags = ["foo"] |
| self._Run(dir_exists=True) |
| self.assertCommandContains([self.cros_sdk], expected=False) |
| self.assertCommandContains(["./update_chroot"], expected=False) |
| |
| |
| class UpdateSDKTest(_RunAbstractStageTestCase): |
| """Test UpdateSDKStage.""" |
| |
| def ConstructStage(self): |
| self.buildstore = FakeBuildStore() |
| return build_stages.UpdateSDKStage( |
| self._run, self.buildstore, self._current_board |
| ) |
| |
| def _RunFull(self, dir_exists=False): |
| """Helper for testing a full builder.""" |
| self._Run(dir_exists) |
| self.assertCommandContains(["./update_chroot"]) |
| |
| def testFullBuildWithProfile(self): |
| """Tests whether full builds add profile flag when requested.""" |
| self._PrepareFull(extra_config={"profile": "foo"}) |
| self._RunFull(dir_exists=False) |
| |
| def testFullBuildWithOverriddenProfile(self): |
| """Tests whether full builds add overridden profile flag when requested.""" |
| self._PrepareFull(extra_cmd_args=["--profile", "smock"]) |
| self._RunFull(dir_exists=False) |
| |
| def _RunBin(self, dir_exists): |
| """Helper for testing a binary builder.""" |
| self._Run(dir_exists) |
| update_nousepkg = self._run.options.latest_toolchain |
| self.assertCommandContains( |
| ["./update_chroot", "--nousepkg"], expected=update_nousepkg |
| ) |
| |
| def testBinBuildWithLatestToolchain(self): |
| """Tests whether we use --nousepkg for creating the board.""" |
| self._PrepareBin() |
| self._run.options.latest_toolchain = True |
| self._RunBin(dir_exists=False) |
| |
| def testBinBuildWithLatestToolchainAndDirExists(self): |
| """Tests whether we use --nousepkg for creating the board.""" |
| self._PrepareBin() |
| self._run.options.latest_toolchain = True |
| self._RunBin(dir_exists=True) |
| |
| |
| class SetupBoardTest(_RunAbstractStageTestCase): |
| """Test building the board""" |
| |
| def setUp(self): |
| self.setup_toolchains_mock = self.PatchObject( |
| commands, "SetupToolchains" |
| ) |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| # Prevent the setup_board tempdir path from being translated because it |
| # ends up raising an error when that path can't be found in the chroot. |
| self.PatchObject(path_util, "ToChrootPath", side_effect=lambda x: x) |
| self.setup_board = os.path.join( |
| self.tempdir, |
| "buildroot", |
| constants.CHROMITE_BIN_SUBDIR, |
| "setup_board", |
| ) |
| |
| def ConstructStage(self): |
| return build_stages.SetupBoardStage( |
| self._run, self.buildstore, self._current_board |
| ) |
| |
| def _RunFull(self, dir_exists=False): |
| """Helper for testing a full builder.""" |
| self._Run(dir_exists) |
| cmd = [ |
| self.setup_board, |
| "--board=%s" % self._current_board, |
| "--nousepkg", |
| ] |
| self.assertCommandContains(cmd) |
| cmd = [self.setup_board, "--skip-chroot-upgrade"] |
| self.assertCommandContains(cmd) |
| |
| def testFullBuildWithProfile(self): |
| """Tests whether full builds add profile flag when requested.""" |
| self._PrepareFull(extra_config={"profile": "foo"}) |
| self._RunFull(dir_exists=False) |
| self.assertCommandContains([self.setup_board, "--profile=foo"]) |
| |
| def testFullBuildWithOverriddenProfile(self): |
| """Tests whether full builds add overridden profile flag when requested.""" |
| self._PrepareFull(extra_cmd_args=["--profile", "smock"]) |
| self._RunFull(dir_exists=False) |
| self.assertCommandContains([self.setup_board, "--profile=smock"]) |
| |
| def _RunBin(self, dir_exists): |
| """Helper for testing a binary builder.""" |
| self._Run(dir_exists) |
| self.assertTrue(self.setup_toolchains_mock.called) |
| self.assertCommandContains([self.setup_board]) |
| cmd = [self.setup_board, "--skip-chroot-upgrade"] |
| self.assertCommandContains(cmd) |
| cmd = [self.setup_board, "--nousepkg"] |
| self.assertCommandContains( |
| cmd, not self._run.config.usepkg_build_packages |
| ) |
| |
| def testBinBuildWithLatestToolchain(self): |
| """Tests whether we use --nousepkg for creating the board.""" |
| self._PrepareBin() |
| self._run.options.latest_toolchain = True |
| self._RunBin(dir_exists=False) |
| |
| def testBinBuildWithLatestToolchainAndDirExists(self): |
| """Tests whether we use --nousepkg for creating the board.""" |
| self._PrepareBin() |
| self._run.options.latest_toolchain = True |
| self._RunBin(dir_exists=True) |
| |
| def testSDKBuild(self): |
| """Tests whether we use --skip_chroot_upgrade for SDK builds.""" |
| extra_config = {"build_type": constants.CHROOT_BUILDER_TYPE} |
| self._PrepareFull(extra_config=extra_config) |
| self._Run(dir_exists=False) |
| self.assertCommandContains(["./update_chroot"], expected=False) |
| self.assertCommandContains([self.setup_board, "--skip-chroot-upgrade"]) |
| |
| |
| class UprevStageTest(generic_stages_unittest.AbstractStageTestCase): |
| """Tests for the UprevStage class.""" |
| |
| def setUp(self): |
| self.uprev_mock = self.PatchObject(commands, "UprevPackages") |
| |
| self._Prepare() |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| def ConstructStage(self): |
| return build_stages.UprevStage(self._run, self.buildstore) |
| |
| def testBuildRev(self): |
| """Uprevving the build without uprevving chrome.""" |
| self._run.config["uprev"] = True |
| self.RunStage() |
| self.assertTrue(self.uprev_mock.called) |
| |
| def testNoRev(self): |
| """No paths are enabled.""" |
| self._run.config["uprev"] = False |
| self.RunStage() |
| self.assertFalse(self.uprev_mock.called) |
| |
| |
| class AllConfigsTestCase( |
| generic_stages_unittest.AbstractStageTestCase, cros_test_lib.OutputTestCase |
| ): |
| """Test case for testing against all bot configs.""" |
| |
| def ConstructStage(self): |
| """Bypass lint warning""" |
| generic_stages_unittest.AbstractStageTestCase.ConstructStage(self) |
| |
| @contextlib.contextmanager |
| def RunStageWithConfig(self, mock_configurator=None): |
| """Run the given config""" |
| try: |
| with cros_test_lib.RunCommandMock() as rc: |
| rc.SetDefaultCmdResult() |
| if mock_configurator: |
| mock_configurator(rc) |
| with self.OutputCapturer(): |
| with cros_test_lib.LoggingCapturer(): |
| self.RunStage() |
| |
| yield rc |
| |
| except AssertionError as ex: |
| msg = "%s failed the following test:\n%s" % (self._bot_id, ex) |
| raise AssertionError(msg) |
| |
| def RunAllConfigs(self, task, site_config=None): |
| """Run |task| against all major configurations""" |
| if site_config is None: |
| site_config = config_lib.GetConfig() |
| |
| boards = ("hatch", "arm-generic") |
| |
| for board in boards: |
| self.CreateMockOverlay(board) |
| |
| with parallel.BackgroundTaskRunner(task) as queue: |
| # Test every build config on an waterfall, that builds something. |
| for bot_id, cfg in site_config.items(): |
| if not cfg.boards or cfg.boards[0] not in boards: |
| continue |
| |
| queue.put([bot_id]) |
| |
| |
| class BuildPackagesStageTest( |
| AllConfigsTestCase, cbuildbot_unittest.SimpleBuilderTestCase |
| ): |
| """Tests BuildPackagesStage.""" |
| |
| def setUp(self): |
| self._release_tag = None |
| self._update_metadata = False |
| self._mock_configurator = None |
| self.PatchObject(commands, "ExtractDependencies", return_value=dict()) |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| def ConstructStage(self): |
| self._run.attrs.release_tag = self._release_tag |
| return build_stages.BuildPackagesStage( |
| self._run, |
| self.buildstore, |
| self._current_board, |
| record_packages_under_test=False, |
| update_metadata=self._update_metadata, |
| ) |
| |
| def RunTestsWithBotId(self, bot_id, options_tests=True): |
| """Test with the config for the specified bot_id.""" |
| self._Prepare(bot_id) |
| self._run.options.tests = options_tests |
| build_packages = os.path.join( |
| self.tempdir, |
| "buildroot", |
| constants.CHROMITE_BIN_SUBDIR, |
| "build_packages", |
| ) |
| |
| with self.RunStageWithConfig(self._mock_configurator) as rc: |
| cfg = self._run.config |
| rc.assertCommandContains([build_packages]) |
| rc.assertCommandContains(["--skip-chroot-upgrade"]) |
| rc.assertCommandContains( |
| ["--no-usepkg"], expected=not cfg["usepkg_build_packages"] |
| ) |
| rc.assertCommandContains( |
| ["--no-withautotest"], expected=not self._run.options.tests |
| ) |
| |
| def testAllConfigs(self): |
| """Test all major configurations""" |
| self.RunAllConfigs(self.RunTestsWithBotId) |
| |
| def testNoTests(self): |
| """Test that self.options.tests = False works.""" |
| self.RunTestsWithBotId("amd64-generic-full", options_tests=False) |
| |
| def testIgnoreExtractDependenciesError(self): |
| """Ignore errors when failing to extract dependencies.""" |
| self.PatchObject( |
| commands, |
| "ExtractDependencies", |
| side_effect=Exception("unmet dependency"), |
| ) |
| self.RunTestsWithBotId("amd64-generic-full") |
| |
| def testFirmwareVersionsMixedImage(self): |
| """Test that firmware versions are extracted correctly.""" |
| expected_main_firmware_version = "reef_v1.1.5822-78709a5" |
| expected_ec_firmware_version = "Google_Reef.9042.30.0" |
| |
| def _HookRunCommandFirmwareUpdate(rc): |
| # A mixed RO+RW image will have separate "(RW) version" fields. |
| rc.AddCmdResult( |
| partial_mock.ListRegex("chromeos-firmwareupdate"), |
| stdout="BIOS (RW) version: %s\nEC (RW) version: %s" |
| % ( |
| expected_main_firmware_version, |
| expected_ec_firmware_version, |
| ), |
| ) |
| |
| self._update_metadata = True |
| update = os.path.join( |
| self.build_root, |
| "chroot/build/amd64-generic/usr/sbin/chromeos-firmwareupdate", |
| ) |
| osutils.Touch(update, makedirs=True) |
| |
| self._mock_configurator = _HookRunCommandFirmwareUpdate |
| self.RunTestsWithBotId("amd64-generic-full", options_tests=False) |
| board_metadata = self._run.attrs.metadata.GetDict()[ |
| "board-metadata" |
| ].get("amd64-generic") |
| if board_metadata: |
| self.assertIn("main-firmware-version", board_metadata) |
| self.assertEqual( |
| board_metadata["main-firmware-version"], |
| expected_main_firmware_version, |
| ) |
| self.assertIn("ec-firmware-version", board_metadata) |
| self.assertEqual( |
| board_metadata["ec-firmware-version"], |
| expected_ec_firmware_version, |
| ) |
| self.assertFalse(self._run.attrs.metadata.GetDict()["unibuild"]) |
| |
| def testFirmwareVersions(self): |
| """Test that firmware versions are extracted correctly.""" |
| expected_main_firmware_version = "reef_v1.1.5822-78709a5" |
| expected_ec_firmware_version = "Google_Reef.9042.30.0" |
| |
| def _HookRunCommandFirmwareUpdate(rc): |
| rc.AddCmdResult( |
| partial_mock.ListRegex("chromeos-firmwareupdate"), |
| stdout="BIOS version: %s\nEC version: %s" |
| % ( |
| expected_main_firmware_version, |
| expected_ec_firmware_version, |
| ), |
| ) |
| |
| self._update_metadata = True |
| update = os.path.join( |
| self.build_root, |
| "chroot/build/amd64-generic/usr/sbin/chromeos-firmwareupdate", |
| ) |
| osutils.Touch(update, makedirs=True) |
| |
| self._mock_configurator = _HookRunCommandFirmwareUpdate |
| self.RunTestsWithBotId("amd64-generic-full", options_tests=False) |
| board_metadata = self._run.attrs.metadata.GetDict()[ |
| "board-metadata" |
| ].get("amd64-generic") |
| if board_metadata: |
| self.assertIn("main-firmware-version", board_metadata) |
| self.assertEqual( |
| board_metadata["main-firmware-version"], |
| expected_main_firmware_version, |
| ) |
| self.assertIn("ec-firmware-version", board_metadata) |
| self.assertEqual( |
| board_metadata["ec-firmware-version"], |
| expected_ec_firmware_version, |
| ) |
| self.assertFalse(self._run.attrs.metadata.GetDict()["unibuild"]) |
| |
| def testFirmwareVersionsUnibuild(self): |
| """Test that firmware versions are extracted correctly for unibuilds.""" |
| |
| def _HookRunCommand(rc): |
| rc.AddCmdResult( |
| partial_mock.In("list-models"), stdout="reef\npyro\nelectro" |
| ) |
| rc.AddCmdResult(partial_mock.In("get"), stdout="key-123") |
| rc.AddCmdResult( |
| partial_mock.ListRegex("chromeos-firmwareupdate"), |
| stdout=""" |
| Model: reef |
| BIOS image: |
| BIOS version: Google_Reef.9042.87.1 |
| BIOS (RW) version: Google_Reef.9042.110.0 |
| EC version: reef_v1.1.5900-ab1ee51 |
| EC (RW) version: reef_v1.1.5909-bd1f0c9 |
| |
| Model: pyro |
| BIOS image: |
| BIOS version: Google_Pyro.9042.87.1 |
| BIOS (RW) version: Google_Pyro.9042.110.0 |
| EC version: pyro_v1.1.5900-ab1ee51 |
| EC (RW) version: pyro_v1.1.5909-bd1f0c9 |
| |
| Model: electro |
| BIOS image: |
| BIOS version: Google_Reef.9042.87.1 |
| EC version: reef_v1.1.5900-ab1ee51 |
| EC (RW) version: reef_v1.1.5909-bd1f0c9 |
| """, |
| ) |
| |
| self._update_metadata = True |
| update = os.path.join( |
| self.build_root, |
| "chroot/build/amd64-generic/usr/sbin/chromeos-firmwareupdate", |
| ) |
| osutils.Touch(update, makedirs=True) |
| |
| cros_config_host = os.path.join( |
| self.build_root, "chroot/usr/bin/cros_config_host" |
| ) |
| osutils.Touch(cros_config_host, makedirs=True) |
| |
| self._mock_configurator = _HookRunCommand |
| self.RunTestsWithBotId("amd64-generic-full", options_tests=False) |
| board_metadata = self._run.attrs.metadata.GetDict()[ |
| "board-metadata" |
| ].get("amd64-generic") |
| self.assertIsNotNone(board_metadata) |
| |
| if "models" in board_metadata: |
| reef = board_metadata["models"]["reef"] |
| self.assertEqual( |
| "Google_Reef.9042.87.1", reef["main-readonly-firmware-version"] |
| ) |
| self.assertEqual( |
| "Google_Reef.9042.110.0", |
| reef["main-readwrite-firmware-version"], |
| ) |
| self.assertEqual( |
| "reef_v1.1.5909-bd1f0c9", reef["ec-firmware-version"] |
| ) |
| self.assertEqual("key-123", reef["firmware-key-id"]) |
| |
| self.assertIn("pyro", board_metadata["models"]) |
| self.assertIn("electro", board_metadata["models"]) |
| electro = board_metadata["models"]["electro"] |
| self.assertEqual( |
| "Google_Reef.9042.87.1", |
| electro["main-readonly-firmware-version"], |
| ) |
| # Test RW firmware is defaulted to RO version if isn't specified. |
| self.assertEqual( |
| "Google_Reef.9042.87.1", |
| electro["main-readwrite-firmware-version"], |
| ) |
| |
| def testUnifiedBuilds(self): |
| """Test that unified builds are marked as such.""" |
| |
| def _HookRunCommandCrosConfigHost(rc): |
| rc.AddCmdResult( |
| partial_mock.ListRegex("cros_config_host"), stdout="reef" |
| ) |
| |
| self._update_metadata = True |
| cros_config_host = os.path.join( |
| self.build_root, "chroot/usr/bin/cros_config_host" |
| ) |
| osutils.Touch(cros_config_host, makedirs=True) |
| self._mock_configurator = _HookRunCommandCrosConfigHost |
| self.RunTestsWithBotId("amd64-generic-full", options_tests=False) |
| self.assertTrue(self._run.attrs.metadata.GetDict()["unibuild"]) |
| |
| def testGoma(self): |
| self.PatchObject( |
| build_stages.BuildPackagesStage, |
| "_ShouldEnableGoma", |
| return_value=True, |
| ) |
| self._Prepare("amd64-generic-full") |
| # Set stub dir name to enable goma. |
| with osutils.TempDir() as goma_dir, tempfile.NamedTemporaryFile() as temp_goma_client_json: |
| goma_dir = Path(goma_dir) |
| goma_client_json = Path(temp_goma_client_json.name) |
| self._run.options.goma_dir = goma_dir |
| self._run.options.goma_client_json = goma_client_json |
| self._run.options.chromeos_goma_dir = goma_dir |
| |
| stage = self.ConstructStage() |
| chroot_args = stage._SetupGomaIfNecessary() |
| self.assertEqual( |
| [ |
| "--goma_dir", |
| str(goma_dir), |
| "--goma_client_json", |
| str(goma_client_json), |
| ], |
| chroot_args, |
| ) |
| portage_env = stage._portage_extra_env |
| self.assertEqual( |
| portage_env.get("GOMA_DIR", ""), os.path.expanduser("~/goma") |
| ) |
| self.assertEqual(portage_env.get("USE_GOMA", ""), "true") |
| self.assertEqual( |
| "/creds/service_accounts/service-account-goma-client.json", |
| portage_env.get("GOMA_SERVICE_ACCOUNT_JSON_FILE", ""), |
| ) |
| |
| def testGomaWithMissingCertFile(self): |
| self.PatchObject( |
| build_stages.BuildPackagesStage, |
| "_ShouldEnableGoma", |
| return_value=True, |
| ) |
| self._Prepare("amd64-generic-full") |
| # Set stub dir name to enable goma. |
| with osutils.TempDir() as goma_dir: |
| self._run.options.goma_dir = goma_dir |
| self._run.options.goma_client_json = "stub-goma-client-json-path" |
| self._run.options.chromeos_goma_dir = goma_dir |
| |
| stage = self.ConstructStage() |
| with self.assertRaisesRegex(ValueError, "json file is missing"): |
| stage._SetupGomaIfNecessary() |
| |
| def testGomaOnBotWithoutCertFile(self): |
| self.PatchObject( |
| build_stages.BuildPackagesStage, |
| "_ShouldEnableGoma", |
| return_value=True, |
| ) |
| self.PatchObject(cros_build_lib, "HostIsCIBuilder", return_value=True) |
| self._Prepare("amd64-generic-full") |
| # Set stub dir name to enable goma. |
| with osutils.TempDir() as goma_dir: |
| self._run.options.goma_dir = goma_dir |
| stage = self.ConstructStage() |
| self._run.options.chromeos_goma_dir = goma_dir |
| chroot_args = stage._SetupGomaIfNecessary() |
| self.assertEqual( |
| [ |
| "--goma_dir", |
| str(goma_dir), |
| ], |
| chroot_args, |
| ) |
| portage_env = stage._portage_extra_env |
| self.assertEqual( |
| portage_env.get("GOMA_DIR", ""), os.path.expanduser("~/goma") |
| ) |
| self.assertEqual( |
| portage_env.get("GOMA_GCE_SERVICE_ACCOUNT", ""), "default" |
| ) |
| |
| |
| class BuildImageStageMock(partial_mock.PartialMock): |
| """Partial mock for BuildImageStage.""" |
| |
| TARGET = "chromite.cbuildbot.stages.build_stages.BuildImageStage" |
| ATTRS = ("_BuildImages",) |
| |
| def _BuildImages(self, *args, **kwargs): |
| with patches( |
| patch(os, "symlink"), patch(os, "readlink", return_value="foo.txt") |
| ): |
| self.backup["_BuildImages"](*args, **kwargs) |
| |
| |
| class BuildImageStageTest(BuildPackagesStageTest): |
| """Tests BuildImageStage.""" |
| |
| def setUp(self): |
| self.StartPatcher(BuildImageStageMock()) |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| def ConstructStage(self): |
| return build_stages.BuildImageStage( |
| self._run, self.buildstore, self._current_board |
| ) |
| |
| def RunTestsWithReleaseConfig(self, release_tag): |
| self._release_tag = release_tag |
| |
| with parallel_unittest.ParallelMock(): |
| with self.RunStageWithConfig() as rc: |
| cfg = self._run.config |
| cmd = [ |
| "./build_image", |
| "--version=%s" % (self._release_tag or ""), |
| ] |
| rc.assertCommandContains(cmd, expected=cfg["images"]) |
| |
| def RunTestsWithBotId(self, bot_id, options_tests=True): |
| """Test with the config for the specified bot_id.""" |
| release_tag = "0.0.1" |
| self._Prepare(bot_id) |
| self._run.options.tests = options_tests |
| self._run.attrs.release_tag = release_tag |
| |
| task = self.RunTestsWithReleaseConfig |
| # TODO: This test is broken atm with tag=None. |
| steps = [lambda tag=x: task(tag) for x in (release_tag,)] |
| parallel.RunParallelSteps(steps) |
| |
| def testUnifiedBuilds(self): |
| pass |
| |
| |
| class CleanUpStageTest(generic_stages_unittest.StageTestCase): |
| """Test CleanUpStage.""" |
| |
| BOT_ID = "amd64-generic-asan" |
| |
| def setUp(self): |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| self.fake_db.InsertBuild( |
| "test_builder", |
| 666, |
| "test_config", |
| "test_hostname", |
| status=constants.BUILDER_STATUS_INFLIGHT, |
| timeout_seconds=23456, |
| buildbucket_id="100", |
| ) |
| |
| self.fake_db.InsertBuild( |
| "test_builder", |
| 666, |
| "test_config", |
| "test_hostname", |
| status=constants.BUILDER_STATUS_INFLIGHT, |
| timeout_seconds=23456, |
| buildbucket_id="200", |
| ) |
| |
| self._Prepare(extra_config={"chroot_use_image": False}) |
| |
| def ConstructStage(self): |
| return build_stages.CleanUpStage(self._run, self.buildstore) |
| |
| def testChrootReuseImageMismatch(self): |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanReuseChroot(chroot_path)) |
| |
| def testChrootReuseChrootReplace(self): |
| self._Prepare( |
| extra_config={"chroot_use_image": False, "chroot_replace": True} |
| ) |
| |
| self.PatchObject( |
| build_stages.CleanUpStage, |
| "_GetPreviousBuildStatus", |
| return_value=build_summary.BuildSummary( |
| build_number=314, status=constants.BUILDER_STATUS_PASSED |
| ), |
| ) |
| |
| chroot_path = os.path.join(self.build_root, "chroot") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanReuseChroot(chroot_path)) |
| |
| def testChrootReusePreviousFailed(self): |
| self.PatchObject( |
| build_stages.CleanUpStage, |
| "_GetPreviousBuildStatus", |
| return_value=build_summary.BuildSummary( |
| build_number=314, status=constants.BUILDER_STATUS_FAILED |
| ), |
| ) |
| |
| chroot_path = os.path.join(self.build_root, "chroot") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanReuseChroot(chroot_path)) |
| |
| def testChrootReusePreviousMasterMissing(self): |
| self.PatchObject( |
| build_stages.CleanUpStage, |
| "_GetPreviousBuildStatus", |
| return_value=build_summary.BuildSummary( |
| build_number=314, |
| master_build_id=2178, |
| status=constants.BUILDER_STATUS_PASSED, |
| ), |
| ) |
| |
| chroot_path = os.path.join(self.build_root, "chroot") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanReuseChroot(chroot_path)) |
| |
| def testChrootReusePreviousMasterFailed(self): |
| master_id = self.fake_db.InsertBuild( |
| "test_builder", |
| 123, |
| "test_config", |
| "test_hostname", |
| status=constants.BUILDER_STATUS_FAILED, |
| buildbucket_id="2178", |
| ) |
| self.PatchObject( |
| build_stages.CleanUpStage, |
| "_GetPreviousBuildStatus", |
| return_value=build_summary.BuildSummary( |
| build_number=314, |
| master_build_id=master_id, |
| status=constants.BUILDER_STATUS_PASSED, |
| ), |
| ) |
| |
| chroot_path = os.path.join(self.build_root, "chroot") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanReuseChroot(chroot_path)) |
| |
| def testChrootReuseAllPassed(self): |
| master_id = self.fake_db.InsertBuild( |
| "test_builder", |
| 123, |
| "test_config", |
| "test_hostname", |
| status=constants.BUILDER_STATUS_PASSED, |
| buildbucket_id="2178", |
| ) |
| self.PatchObject( |
| build_stages.CleanUpStage, |
| "_GetPreviousBuildStatus", |
| return_value=build_summary.BuildSummary( |
| build_number=314, |
| master_build_id=master_id, |
| status=constants.BUILDER_STATUS_PASSED, |
| ), |
| ) |
| |
| chroot_path = os.path.join(self.build_root, "chroot") |
| stage = self.ConstructStage() |
| self.assertTrue(stage.CanReuseChroot(chroot_path)) |
| |
| def testChrootSnapshotClobber(self): |
| self._Prepare( |
| extra_cmd_args=["--clobber"], |
| extra_config={"chroot_use_image": True, "chroot_replace": False}, |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanUseChrootSnapshotToDelete(chroot_path)) |
| |
| def testChrootSnapshotReplace(self): |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": True} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanUseChrootSnapshotToDelete(chroot_path)) |
| |
| def testChrootSnapshotNoUseImage(self): |
| self._Prepare( |
| extra_config={"chroot_use_image": False, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanUseChrootSnapshotToDelete(chroot_path)) |
| |
| def testChrootSnapshotMissingImage(self): |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| stage = self.ConstructStage() |
| self.assertFalse(stage.CanUseChrootSnapshotToDelete(chroot_path)) |
| |
| def testChrootSnapshotAllPass(self): |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertTrue(stage.CanUseChrootSnapshotToDelete(chroot_path)) |
| |
| def testChrootRevertNoSnapshots(self): |
| self.PatchObject(commands, "ListChrootSnapshots", return_value=[]) |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage._RevertChrootToCleanSnapshot()) |
| |
| def testChrootRevertSnapshotNotFound(self): |
| self.PatchObject(commands, "ListChrootSnapshots", return_value=["snap"]) |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage._RevertChrootToCleanSnapshot()) |
| |
| def testChrootCleanSnapshotReplacesAllExisting(self): |
| self.PatchObject( |
| commands, |
| "ListChrootSnapshots", |
| return_value=["snap1", "snap2", constants.CHROOT_SNAPSHOT_CLEAN], |
| ) |
| delete_mock = self.PatchObject( |
| commands, "DeleteChrootSnapshot", return_value=True |
| ) |
| create_mock = self.PatchObject(commands, "CreateChrootSnapshot") |
| |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| stage._CreateCleanSnapshot() |
| |
| self.assertEqual( |
| delete_mock.mock_calls, |
| [ |
| mock.call(self.build_root, "snap1"), |
| mock.call(self.build_root, "snap2"), |
| mock.call(self.build_root, constants.CHROOT_SNAPSHOT_CLEAN), |
| ], |
| ) |
| create_mock.assert_called_with( |
| self.build_root, constants.CHROOT_SNAPSHOT_CLEAN |
| ) |
| |
| def testChrootRevertFailsWhenCommandsRaiseExceptions(self): |
| self.PatchObject( |
| cros_build_lib, |
| "sudo_run", |
| side_effect=cros_build_lib.RunCommandError( |
| "error", cros_build_lib.CompletedProcess("error", returncode=5) |
| ), |
| ) |
| self._Prepare( |
| extra_config={"chroot_use_image": True, "chroot_replace": False} |
| ) |
| chroot_path = os.path.join(self.build_root, "chroot") |
| osutils.Touch(chroot_path + ".img") |
| stage = self.ConstructStage() |
| self.assertFalse(stage._RevertChrootToCleanSnapshot()) |
| |
| |
| class CleanUpStageCancelSlaveBuilds(generic_stages_unittest.StageTestCase): |
| """Test CleanUpStage.CancelObsoleteSlaveBuilds.""" |
| |
| BOT_ID = "master-full" |
| |
| def setUp(self): |
| # Mock out the active APIs for both testing and safety. |
| self.cancelMock = self.PatchObject( |
| buildbucket_v2.BuildbucketV2, "BatchCancelBuilds" |
| ) |
| self.searchMock = self.PatchObject( |
| buildbucket_v2.BuildbucketV2, "BatchSearchBuilds" |
| ) |
| |
| self._Prepare(extra_config={"chroot_use_image": False}) |
| self.fake_db = fake_cidb.FakeCIDBConnection() |
| self.buildstore = FakeBuildStore(self.fake_db) |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.fake_db) |
| |
| def ConstructStage(self): |
| return build_stages.CleanUpStage(self._run, self.buildstore) |
| |
| def testNoPreviousMasterBuilds(self): |
| """Test cancellation if the master has never run.""" |
| search_results = builds_service_pb2.BatchResponse( |
| responses=[ |
| builds_service_pb2.BatchResponse.Response( |
| search_builds=builds_service_pb2.SearchBuildsResponse( |
| builds=[], |
| ), |
| ), |
| ], |
| ) |
| self.searchMock.return_value = search_results |
| cancel_results = builds_service_pb2.BatchResponse( |
| responses=[], |
| ) |
| self.cancelMock.return_value = cancel_results |
| stage = self.ConstructStage() |
| stage.CancelObsoleteSlaveBuilds() |
| |
| # Validate searches and cancellations match expectations. |
| self.assertEqual( |
| len(self.searchMock.return_value.responses[0].search_builds.builds), |
| 0, |
| ) |
| self.assertEqual(len(self.cancelMock.return_value.responses), 0) |
| |
| def testNoPreviousSlaveBuilds(self): |
| """Test cancellation if there are no running slave builds.""" |
| search_results = builds_service_pb2.BatchResponse( |
| responses=[ |
| builds_service_pb2.BatchResponse.Response( |
| search_builds=builds_service_pb2.SearchBuildsResponse( |
| builds=[build_pb2.Build(id=1)], |
| ), |
| ) |
| ], |
| ) |
| self.searchMock.return_value = search_results |
| cancel_results = builds_service_pb2.BatchResponse( |
| responses=[], |
| ) |
| self.cancelMock.return_value = cancel_results |
| stage = self.ConstructStage() |
| stage.CancelObsoleteSlaveBuilds() |
| |
| # Validate searches and cancellations match expectations. |
| self.assertEqual(len(self.searchMock.return_value.responses), 1) |
| self.assertEqual(len(self.cancelMock.return_value.responses), 0) |
| |
| def testPreviousSlaveBuild(self): |
| """Test cancellation if there is a running slave build.""" |
| search_results = builds_service_pb2.BatchResponse( |
| responses=[ |
| builds_service_pb2.BatchResponse.Response( |
| search_builds=builds_service_pb2.SearchBuildsResponse( |
| builds=[ |
| build_pb2.Build(id=1, status="SUCCESS"), |
| build_pb2.Build(id=2, status="SUCCESS"), |
| ], |
| ), |
| ) |
| ], |
| ) |
| self.searchMock.return_value = search_results |
| stage = self.ConstructStage() |
| stage.CancelObsoleteSlaveBuilds() |
| |
| # Validate searches and cancellations match expectations. |
| self.assertEqual( |
| len(self.searchMock.return_value.responses[0].search_builds.builds), |
| 2, |
| ) |
| self.assertEqual(self.cancelMock.call_count, 1) |
| |
| cancelled_ids = self.cancelMock.call_args[0][0] |
| self.assertEqual(cancelled_ids, [1, 2]) |
| |
| def testManyPreviousSlaveBuilds(self): |
| """Test cancellation with an assortment of running slave builds.""" |
| search_results = builds_service_pb2.BatchResponse( |
| responses=[ |
| builds_service_pb2.BatchResponse.Response( |
| search_builds=builds_service_pb2.SearchBuildsResponse( |
| builds=[ |
| build_pb2.Build(id=1, status="SUCCESS"), |
| build_pb2.Build(id=2, status="SUCCESS"), |
| build_pb2.Build(id=3, status="SUCCESS"), |
| build_pb2.Build(id=4, status="SUCCESS"), |
| ], |
| ), |
| ) |
| ], |
| ) |
| self.searchMock.return_value = search_results |
| stage = self.ConstructStage() |
| stage.CancelObsoleteSlaveBuilds() |
| |
| # Validate searches and cancellations match expectations. |
| self.assertEqual( |
| len(self.searchMock.return_value.responses[0].search_builds.builds), |
| 4, |
| ) |
| self.assertEqual(self.cancelMock.call_count, 1) |
| |
| cancelled_ids = self.cancelMock.call_args[0][0] |
| self.assertEqual(cancelled_ids, [1, 2, 3, 4]) |