| # Copyright 2019 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Sysroot controller tests.""" | |
| import datetime | |
| import os | |
| from typing import Union | |
| from chromite.api import api_config | |
| from chromite.api import controller | |
| from chromite.api.controller import controller_util | |
| from chromite.api.controller import sysroot as sysroot_controller | |
| from chromite.api.gen.chromite.api import sysroot_pb2 | |
| from chromite.api.gen.chromiumos import common_pb2 | |
| from chromite.lib import cros_build_lib | |
| from chromite.lib import cros_test_lib | |
| from chromite.lib import osutils | |
| from chromite.lib import sysroot_lib | |
| from chromite.lib.parser import package_info | |
| from chromite.service import sysroot as sysroot_service | |
| class CreateTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin): | |
| """Create function tests.""" | |
| def _InputProto( | |
| self, | |
| build_target=None, | |
| profile=None, | |
| replace=False, | |
| current=False, | |
| use_cq_prebuilts=False, | |
| package_indexes=None, | |
| ): | |
| """Helper to build and input proto instance.""" | |
| proto = sysroot_pb2.SysrootCreateRequest() | |
| if build_target: | |
| proto.build_target.name = build_target | |
| if profile: | |
| proto.profile.name = profile | |
| if replace: | |
| proto.flags.replace = replace | |
| if current: | |
| proto.flags.chroot_current = current | |
| if use_cq_prebuilts: | |
| proto.flags.use_cq_prebuilts = use_cq_prebuilts | |
| if package_indexes: | |
| proto.package_indexes.extend(package_indexes) | |
| return proto | |
| def _OutputProto(self): | |
| """Helper to build output proto instance.""" | |
| return sysroot_pb2.SysrootCreateResponse() | |
| def testValidateOnly(self): | |
| """Verify a validate-only call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "Create") | |
| board = "board" | |
| profile = None | |
| force = False | |
| upgrade_chroot = True | |
| in_proto = self._InputProto( | |
| build_target=board, | |
| profile=profile, | |
| replace=force, | |
| current=not upgrade_chroot, | |
| ) | |
| sysroot_controller.Create( | |
| in_proto, self._OutputProto(), self.validate_only_config | |
| ) | |
| patch.assert_not_called() | |
| def testMockCall(self): | |
| """Verify a mock call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "Create") | |
| request = self._InputProto() | |
| response = self._OutputProto() | |
| rc = sysroot_controller.Create(request, response, self.mock_call_config) | |
| patch.assert_not_called() | |
| self.assertEqual(controller.RETURN_CODE_SUCCESS, rc) | |
| def testMockError(self): | |
| """Verify a mock error does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "Create") | |
| request = self._InputProto() | |
| response = self._OutputProto() | |
| rc = sysroot_controller.Create( | |
| request, response, self.mock_error_config | |
| ) | |
| patch.assert_not_called() | |
| self.assertEqual(controller.RETURN_CODE_UNRECOVERABLE, rc) | |
| def testArgumentValidation(self): | |
| """Test the input argument validation.""" | |
| # Error when no name provided. | |
| in_proto = self._InputProto() | |
| out_proto = self._OutputProto() | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.Create(in_proto, out_proto, self.api_config) | |
| # Valid when board passed. | |
| result = sysroot_lib.Sysroot("/sysroot/path") | |
| patch = self.PatchObject(sysroot_service, "Create", return_value=result) | |
| in_proto = self._InputProto("board") | |
| out_proto = self._OutputProto() | |
| sysroot_controller.Create(in_proto, out_proto, self.api_config) | |
| patch.assert_called_once() | |
| def testArgumentHandling(self): | |
| """Test the arguments get processed and passed correctly.""" | |
| sysroot_path = "/sysroot/path" | |
| sysroot = sysroot_lib.Sysroot(sysroot_path) | |
| create_patch = self.PatchObject( | |
| sysroot_service, "Create", return_value=sysroot | |
| ) | |
| rc_patch = self.PatchObject(sysroot_service, "SetupBoardRunConfig") | |
| # Default values. | |
| board = "board" | |
| profile = None | |
| force = False | |
| upgrade_chroot = True | |
| use_cq_prebuilts = False | |
| in_proto = self._InputProto( | |
| build_target=board, | |
| profile=profile, | |
| replace=force, | |
| current=not upgrade_chroot, | |
| use_cq_prebuilts=use_cq_prebuilts, | |
| ) | |
| out_proto = self._OutputProto() | |
| sysroot_controller.Create(in_proto, out_proto, self.api_config) | |
| # Default value checks. | |
| rc_patch.assert_called_with( | |
| force=force, | |
| upgrade_chroot=upgrade_chroot, | |
| package_indexes=[], | |
| use_cq_prebuilts=use_cq_prebuilts, | |
| backtrack=sysroot_controller.DEFAULT_BACKTRACK, | |
| ) | |
| self.assertEqual(board, out_proto.sysroot.build_target.name) | |
| self.assertEqual(sysroot_path, out_proto.sysroot.path) | |
| # Not default values. | |
| create_patch.reset_mock() | |
| board = "board" | |
| profile = "profile" | |
| force = True | |
| upgrade_chroot = False | |
| use_cq_prebuilts = True | |
| package_indexes = [ | |
| common_pb2.PackageIndexInfo( | |
| snapshot_sha="SHA", | |
| snapshot_number=5, | |
| build_target=common_pb2.BuildTarget(name=board), | |
| location="LOCATION", | |
| profile=common_pb2.Profile(name=profile), | |
| ), | |
| common_pb2.PackageIndexInfo( | |
| snapshot_sha="SHA2", | |
| snapshot_number=4, | |
| build_target=common_pb2.BuildTarget(name=board), | |
| location="LOCATION2", | |
| profile=common_pb2.Profile(name=profile), | |
| ), | |
| ] | |
| in_proto = self._InputProto( | |
| build_target=board, | |
| profile=profile, | |
| replace=force, | |
| current=not upgrade_chroot, | |
| use_cq_prebuilts=use_cq_prebuilts, | |
| package_indexes=package_indexes, | |
| ) | |
| out_proto = self._OutputProto() | |
| sysroot_controller.Create(in_proto, out_proto, self.api_config) | |
| # Not default value checks. | |
| rc_patch.assert_called_with( | |
| force=force, | |
| package_indexes=[ | |
| controller_util.deserialize_package_index_info(x) | |
| for x in package_indexes | |
| ], | |
| upgrade_chroot=upgrade_chroot, | |
| use_cq_prebuilts=use_cq_prebuilts, | |
| backtrack=sysroot_controller.DEFAULT_BACKTRACK, | |
| ) | |
| self.assertEqual(board, out_proto.sysroot.build_target.name) | |
| self.assertEqual(sysroot_path, out_proto.sysroot.path) | |
| class GetArtifactsTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin): | |
| """GetArtifacts function tests.""" | |
| # pylint: disable=line-too-long | |
| _artifact_funcs = { | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.SIMPLE_CHROME_SYSROOT: sysroot_service.CreateSimpleChromeSysroot, | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.CHROME_EBUILD_ENV: sysroot_service.CreateChromeEbuildEnv, | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.BREAKPAD_DEBUG_SYMBOLS: sysroot_service.BundleBreakpadSymbols, | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.DEBUG_SYMBOLS: sysroot_service.BundleDebugSymbols, | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.FUZZER_SYSROOT: sysroot_service.CreateFuzzerSysroot, | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.SYSROOT_ARCHIVE: sysroot_service.ArchiveSysroot, | |
| } | |
| # pylint: enable=line-too-long | |
| def setUp(self): | |
| self._mocks = {} | |
| for artifact, func in self._artifact_funcs.items(): | |
| self._mocks[artifact] = self.PatchObject( | |
| sysroot_service, func.__name__ | |
| ) | |
| def _InputProto( | |
| self, | |
| artifact_types=_artifact_funcs.keys(), | |
| ): | |
| """Helper to build an input proto instance.""" | |
| return common_pb2.ArtifactsByService.Sysroot( | |
| output_artifacts=[ | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactInfo( | |
| artifact_types=artifact_types | |
| ) | |
| ] | |
| ) | |
| def testNoArtifacts(self): | |
| """Test GetArtifacts with no artifact types.""" | |
| in_proto = self._InputProto(artifact_types=[]) | |
| sysroot_controller.GetArtifacts( | |
| in_proto, None, None, "build_target", "" | |
| ) | |
| for _, patch in self._mocks.items(): | |
| patch.assert_not_called() | |
| def testArtifactsSuccess(self): | |
| """Test GetArtifacts with all artifact types.""" | |
| sysroot_controller.GetArtifacts( | |
| self._InputProto(), None, None, "build_target", "" | |
| ) | |
| for _, patch in self._mocks.items(): | |
| patch.assert_called_once() | |
| def testArtifactsException(self): | |
| """Test with all artifact types when one type throws an exception.""" | |
| self._mocks[ | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.FUZZER_SYSROOT | |
| ].side_effect = Exception("foo bar") | |
| generated = sysroot_controller.GetArtifacts( | |
| self._InputProto(), None, None, "build_target", "" | |
| ) | |
| for _, patch in self._mocks.items(): | |
| patch.assert_called_once() | |
| found_artifact = False | |
| for data in generated: | |
| artifact_type = ( | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.Name( | |
| data["type"] | |
| ) | |
| ) | |
| if artifact_type == "FUZZER_SYSROOT": | |
| found_artifact = True | |
| self.assertTrue(data["failed"]) | |
| self.assertEqual(data["failure_reason"], "foo bar") | |
| self.assertTrue(found_artifact) | |
| def testArtifactsBreakpadDebugSymbols(self): | |
| """Tests the extra parameters to BundleBreakpadSymbols""" | |
| proto = common_pb2.ArtifactsByService.Sysroot( | |
| output_artifacts=[ | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactInfo( | |
| artifact_types=[ | |
| # pylint: disable=line-too-long | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.BREAKPAD_DEBUG_SYMBOLS | |
| # pylint: enable=line-too-long | |
| ] | |
| ) | |
| ], | |
| ignore_breakpad_symbol_generation_errors=True, | |
| ignore_breakpad_symbol_generation_expected_files=[ | |
| # pylint: disable=line-too-long | |
| common_pb2.ArtifactsByService.Sysroot.BreakpadSymbolGenerationExpectedFile.EXPECTED_FILE_LIBC, | |
| common_pb2.ArtifactsByService.Sysroot.BreakpadSymbolGenerationExpectedFile.EXPECTED_FILE_CRASH_REPORTER, | |
| # pylint: enable=line-too-long | |
| ], | |
| ) | |
| sysroot_controller.GetArtifacts( | |
| proto, None, None, "build_target", "out" | |
| ) | |
| self._mocks[ | |
| # pylint: disable=line-too-long | |
| common_pb2.ArtifactsByService.Sysroot.ArtifactType.BREAKPAD_DEBUG_SYMBOLS | |
| # pylint: enable=line-too-long | |
| ].assert_called_once_with( | |
| None, | |
| None, | |
| "build_target", | |
| "out", | |
| True, | |
| ["LIBC", "CRASH_REPORTER"], | |
| ) | |
| def testArtifactsExpectedFileNames(self): | |
| """Verify all BreakpadSymbolGenerationExpectedFile have valid names. | |
| _BundleBreakpadSymbols inside GetArtifacts assumes that all values of | |
| the BreakpadSymbolGenerationExpectedFile enum are named starting with | |
| EXPECTED_FILE_. Confirm that assumption. | |
| """ | |
| for enum in ( | |
| # pylint: disable=line-too-long | |
| common_pb2.ArtifactsByService.Sysroot.BreakpadSymbolGenerationExpectedFile.keys() | |
| # pylint: enable=line-too-long | |
| ): | |
| self.assertTrue(enum.startswith("EXPECTED_FILE_")) | |
| class GenerateArchiveTest( | |
| cros_test_lib.MockTempDirTestCase, api_config.ApiConfigMixin | |
| ): | |
| """GenerateArchive function tests.""" | |
| def setUp(self): | |
| self.chroot_path = "/path/to/chroot" | |
| self.board = "board" | |
| def _InputProto(self, build_target=None, chroot_path=None, pkg_list=None): | |
| """Helper to build and input proto instance.""" | |
| # pkg_list will be a list of category/package strings such as | |
| # ['virtual/target-fuzzers']. | |
| if pkg_list: | |
| package_list = [] | |
| for pkg in pkg_list: | |
| pkg_string_parts = pkg.split("/") | |
| package_info_msg = common_pb2.PackageInfo( | |
| category=pkg_string_parts[0], | |
| package_name=pkg_string_parts[1], | |
| ) | |
| package_list.append(package_info_msg) | |
| else: | |
| package_list = [] | |
| return sysroot_pb2.SysrootGenerateArchiveRequest( | |
| build_target={"name": build_target}, | |
| chroot={"path": chroot_path}, | |
| packages=package_list, | |
| ) | |
| def _OutputProto(self): | |
| """Helper to build output proto instance.""" | |
| return sysroot_pb2.SysrootGenerateArchiveResponse() | |
| def testValidateOnly(self): | |
| """Verify a validate-only call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "GenerateArchive") | |
| in_proto = self._InputProto( | |
| build_target=self.board, | |
| chroot_path=self.chroot_path, | |
| pkg_list=["virtual/target-fuzzers"], | |
| ) | |
| sysroot_controller.GenerateArchive( | |
| in_proto, self._OutputProto(), self.validate_only_config | |
| ) | |
| patch.assert_not_called() | |
| def testMockCall(self): | |
| """Sanity check that a mock call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "GenerateArchive") | |
| in_proto = self._InputProto( | |
| build_target=self.board, | |
| chroot_path=self.chroot_path, | |
| pkg_list=["virtual/target-fuzzers"], | |
| ) | |
| sysroot_controller.GenerateArchive( | |
| in_proto, self._OutputProto(), self.mock_call_config | |
| ) | |
| patch.assert_not_called() | |
| def testArgumentValidation(self): | |
| """Test the input argument validation.""" | |
| # Error when no build target provided. | |
| in_proto = self._InputProto() | |
| out_proto = self._OutputProto() | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.GenerateArchive( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # Error when packages is not specified. | |
| in_proto = self._InputProto( | |
| build_target="board", chroot_path=self.chroot_path | |
| ) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.GenerateArchive( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # Valid when board, chroot path, and package are specified. | |
| patch = self.PatchObject( | |
| sysroot_service, | |
| "GenerateArchive", | |
| return_value="/path/to/sysroot/tar.bz", | |
| ) | |
| in_proto = self._InputProto( | |
| build_target="board", | |
| chroot_path=self.chroot_path, | |
| pkg_list=["virtual/target-fuzzers"], | |
| ) | |
| out_proto = self._OutputProto() | |
| sysroot_controller.GenerateArchive(in_proto, out_proto, self.api_config) | |
| patch.assert_called_once() | |
| class InstallToolchainTest( | |
| cros_test_lib.MockTempDirTestCase, api_config.ApiConfigMixin | |
| ): | |
| """Install toolchain function tests.""" | |
| def setUp(self): | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=True) | |
| # Avoid running the portageq command. | |
| self.PatchObject(sysroot_controller, "_LogBinhost") | |
| self.board = "board" | |
| self.sysroot = os.path.join(self.tempdir, "board") | |
| self.invalid_sysroot = os.path.join(self.tempdir, "invalid", "sysroot") | |
| osutils.SafeMakedirs(self.sysroot) | |
| # Set up portage log directory. | |
| self.target_sysroot = sysroot_lib.Sysroot(self.sysroot) | |
| self.portage_dir = os.path.join(self.tempdir, "portage_logdir") | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "portage_logdir", new=self.portage_dir | |
| ) | |
| osutils.SafeMakedirs(self.portage_dir) | |
| def _InputProto( | |
| self, build_target=None, sysroot_path=None, compile_source=False | |
| ): | |
| """Helper to build an input proto instance.""" | |
| proto = sysroot_pb2.InstallToolchainRequest() | |
| if build_target: | |
| proto.sysroot.build_target.name = build_target | |
| if sysroot_path: | |
| proto.sysroot.path = sysroot_path | |
| if compile_source: | |
| proto.flags.compile_source = compile_source | |
| return proto | |
| def _OutputProto(self): | |
| """Helper to build output proto instance.""" | |
| return sysroot_pb2.InstallToolchainResponse() | |
| def _CreatePortageLogFile( | |
| self, | |
| log_path: Union[str, os.PathLike], | |
| pkg_info: package_info.PackageInfo, | |
| timestamp: datetime.datetime, | |
| ): | |
| """Creates a log file to test for individual packages built by Portage. | |
| Args: | |
| log_path: The PORTAGE_LOGDIR path. | |
| pkg_info: Package name used to name the log file. | |
| timestamp: Timestamp used to name the file. | |
| """ | |
| path = os.path.join( | |
| log_path, | |
| f"{pkg_info.category}:{pkg_info.pvr}:" | |
| f'{timestamp.strftime("%Y%m%d-%H%M%S")}.log', | |
| ) | |
| osutils.WriteFile( | |
| path, | |
| f"Test log file for package {pkg_info.category}/" | |
| f"{pkg_info.package} written to {path}", | |
| ) | |
| return path | |
| def testValidateOnly(self): | |
| """Verify a validate-only call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "InstallToolchain") | |
| in_proto = self._InputProto( | |
| build_target=self.board, sysroot_path=self.sysroot | |
| ) | |
| sysroot_controller.InstallToolchain( | |
| in_proto, self._OutputProto(), self.validate_only_config | |
| ) | |
| patch.assert_not_called() | |
| def testMockCall(self): | |
| """Sanity check that a mock call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "InstallToolchain") | |
| request = self._InputProto() | |
| response = self._OutputProto() | |
| rc = sysroot_controller.InstallToolchain( | |
| request, response, self.mock_call_config | |
| ) | |
| patch.assert_not_called() | |
| self.assertEqual(controller.RETURN_CODE_SUCCESS, rc) | |
| def testMockError(self): | |
| """Sanity check that a mock error does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "InstallToolchain") | |
| request = self._InputProto() | |
| response = self._OutputProto() | |
| rc = sysroot_controller.InstallToolchain( | |
| request, response, self.mock_error_config | |
| ) | |
| patch.assert_not_called() | |
| self.assertEqual( | |
| controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc | |
| ) | |
| self.assertTrue(response.failed_package_data) | |
| def testArgumentValidation(self): | |
| """Test the argument validation.""" | |
| # Test errors on missing inputs. | |
| out_proto = self._OutputProto() | |
| # Both missing. | |
| in_proto = self._InputProto() | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallToolchain( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # Sysroot path missing. | |
| in_proto = self._InputProto(build_target=self.board) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallToolchain( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # Build target name missing. | |
| in_proto = self._InputProto(sysroot_path=self.sysroot) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallToolchain( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # Both provided, but invalid sysroot path. | |
| in_proto = self._InputProto( | |
| build_target=self.board, sysroot_path=self.invalid_sysroot | |
| ) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallToolchain( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| def testSuccessOutputHandling(self): | |
| """Test the output is processed and recorded correctly.""" | |
| self.PatchObject(sysroot_service, "InstallToolchain") | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto( | |
| build_target=self.board, sysroot_path=self.sysroot | |
| ) | |
| rc = sysroot_controller.InstallToolchain( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertFalse(rc) | |
| self.assertFalse(out_proto.failed_package_data) | |
| def testErrorOutputHandling(self): | |
| """Test the error output is processed and recorded correctly.""" | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto( | |
| build_target=self.board, sysroot_path=self.sysroot | |
| ) | |
| err_pkgs = ["cat/pkg-1.0-r1", "cat2/pkg2-1.0-r1"] | |
| err_cpvs = [package_info.parse(pkg) for pkg in err_pkgs] | |
| expected = [("cat", "pkg"), ("cat2", "pkg2")] | |
| new_logs = {} | |
| for i, pkg in enumerate(err_pkgs): | |
| self._CreatePortageLogFile( | |
| self.portage_dir, | |
| err_cpvs[i], | |
| datetime.datetime(2021, 6, 9, 13, 37, 0), | |
| ) | |
| new_logs[pkg] = self._CreatePortageLogFile( | |
| self.portage_dir, | |
| err_cpvs[i], | |
| datetime.datetime(2021, 6, 9, 16, 20, 0), | |
| ) | |
| err = sysroot_lib.ToolchainInstallError( | |
| "Error", cros_build_lib.CompletedProcess(), tc_info=err_cpvs | |
| ) | |
| self.PatchObject(sysroot_service, "InstallToolchain", side_effect=err) | |
| rc = sysroot_controller.InstallToolchain( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertEqual( | |
| controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc | |
| ) | |
| self.assertTrue(out_proto.failed_package_data) | |
| # This needs to return 2 to indicate the available error response. | |
| self.assertEqual( | |
| controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc | |
| ) | |
| for data in out_proto.failed_package_data: | |
| package = controller_util.deserialize_package_info(data.name) | |
| cat_pkg = (data.name.category, data.name.package_name) | |
| self.assertIn(cat_pkg, expected) | |
| self.assertEqual(data.log_path.path, new_logs[package.cpvr]) | |
| class InstallPackagesTest( | |
| cros_test_lib.MockTempDirTestCase, api_config.ApiConfigMixin | |
| ): | |
| """InstallPackages tests.""" | |
| def setUp(self): | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=True) | |
| # Avoid running the portageq command. | |
| self.PatchObject(sysroot_controller, "_LogBinhost") | |
| self.build_target = "board" | |
| self.sysroot = os.path.join(self.tempdir, "build", "board") | |
| osutils.SafeMakedirs(self.sysroot) | |
| # Set up portage log directory. | |
| self.target_sysroot = sysroot_lib.Sysroot(self.sysroot) | |
| self.portage_dir = os.path.join(self.tempdir, "portage_logdir") | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "portage_logdir", new=self.portage_dir | |
| ) | |
| osutils.SafeMakedirs(self.portage_dir) | |
| # Set up goma directories. | |
| self.goma_dir = os.path.join(self.tempdir, "goma_dir") | |
| osutils.SafeMakedirs(self.goma_dir) | |
| self.goma_out_dir = os.path.join(self.tempdir, "goma_out_dir") | |
| osutils.SafeMakedirs(self.goma_out_dir) | |
| os.environ["GLOG_log_dir"] = self.goma_dir | |
| def _InputProto( | |
| self, | |
| build_target=None, | |
| sysroot_path=None, | |
| build_source=False, | |
| use_cq_prebuilts=False, | |
| goma_dir=None, | |
| goma_log_dir=None, | |
| goma_stats_file=None, | |
| goma_counterz_file=None, | |
| package_indexes=None, | |
| packages=None, | |
| bazel=False, | |
| ): | |
| """Helper to build an input proto instance.""" | |
| instance = sysroot_pb2.InstallPackagesRequest() | |
| if build_target: | |
| instance.sysroot.build_target.name = build_target | |
| if sysroot_path: | |
| instance.sysroot.path = sysroot_path | |
| if build_source: | |
| instance.flags.build_source = build_source | |
| if use_cq_prebuilts: | |
| instance.flags.use_cq_prebuilts = use_cq_prebuilts | |
| if goma_dir: | |
| instance.goma_config.goma_dir = goma_dir | |
| if goma_log_dir: | |
| instance.goma_config.log_dir.dir = goma_log_dir | |
| if goma_stats_file: | |
| instance.goma_config.stats_file = goma_stats_file | |
| if goma_counterz_file: | |
| instance.goma_config.counterz_file = goma_counterz_file | |
| if package_indexes: | |
| instance.package_indexes.extend(package_indexes) | |
| if packages: | |
| for pkg in packages: | |
| pkg_info = package_info.parse(pkg) | |
| pkg_info_msg = instance.packages.add() | |
| controller_util.serialize_package_info(pkg_info, pkg_info_msg) | |
| if bazel: | |
| instance.flags.bazel = bazel | |
| return instance | |
| def _OutputProto(self): | |
| """Helper to build an empty output proto instance.""" | |
| return sysroot_pb2.InstallPackagesResponse() | |
| def _CreateGomaLogFile( | |
| self, | |
| goma_log_dir: Union[str, os.PathLike], | |
| name: str, | |
| timestamp: datetime.datetime, | |
| ): | |
| """Creates a log file for testing. | |
| Args: | |
| goma_log_dir: Directory where the file will be created. | |
| name: Log file 'base' name that is combined with the timestamp. | |
| timestamp: Timestamp that is written to the file. | |
| """ | |
| path = os.path.join( | |
| goma_log_dir, | |
| "%s.host.log.INFO.%s" | |
| % (name, timestamp.strftime("%Y%m%d-%H%M%S.%f")), | |
| ) | |
| osutils.WriteFile( | |
| path, | |
| timestamp.strftime("Goma log file created at: %Y/%m/%d %H:%M:%S"), | |
| ) | |
| def _CreatePortageLogFile( | |
| self, | |
| log_path: Union[str, os.PathLike], | |
| pkg_info: package_info.PackageInfo, | |
| timestamp: datetime.datetime, | |
| ): | |
| """Creates a log file to test for individual packages built by Portage. | |
| Args: | |
| log_path: The PORTAGE_LOGDIR path. | |
| pkg_info: Package name used to name the log file. | |
| timestamp: Timestamp used to name the file. | |
| """ | |
| path = os.path.join( | |
| log_path, | |
| f"{pkg_info.category}:{pkg_info.pvr}:" | |
| f'{timestamp.strftime("%Y%m%d-%H%M%S")}.log', | |
| ) | |
| osutils.WriteFile( | |
| path, | |
| f"Test log file for package {pkg_info.category}/" | |
| f"{pkg_info.package} written to {path}", | |
| ) | |
| return path | |
| def testValidateOnly(self): | |
| """Verify a validate-only call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "BuildPackages") | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, sysroot_path=self.sysroot | |
| ) | |
| sysroot_controller.InstallPackages( | |
| in_proto, self._OutputProto(), self.validate_only_config | |
| ) | |
| patch.assert_not_called() | |
| def testMockCall(self): | |
| """Sanity check that a mock call does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "BuildPackages") | |
| request = self._InputProto() | |
| response = self._OutputProto() | |
| rc = sysroot_controller.InstallPackages( | |
| request, response, self.mock_call_config | |
| ) | |
| patch.assert_not_called() | |
| self.assertEqual(controller.RETURN_CODE_SUCCESS, rc) | |
| def testMockError(self): | |
| """Sanity check that a mock error does not execute any logic.""" | |
| patch = self.PatchObject(sysroot_service, "BuildPackages") | |
| request = self._InputProto() | |
| response = self._OutputProto() | |
| rc = sysroot_controller.InstallPackages( | |
| request, response, self.mock_error_config | |
| ) | |
| patch.assert_not_called() | |
| self.assertEqual( | |
| controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc | |
| ) | |
| self.assertTrue(response.failed_package_data) | |
| def testArgumentValidationAllMissing(self): | |
| """Test missing all arguments.""" | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto() | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| def testArgumentValidationNoSysroot(self): | |
| """Test missing sysroot path.""" | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto(build_target=self.build_target) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| def testArgumentValidationNoBuildTarget(self): | |
| """Test missing build target name.""" | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto(sysroot_path=self.sysroot) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| def testArgumentValidationInvalidSysroot(self): | |
| """Test sysroot that hasn't had the toolchain installed.""" | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, sysroot_path=self.sysroot | |
| ) | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=False | |
| ) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| def testArgumentValidationInvalidPackage(self): | |
| out_proto = self._OutputProto() | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, | |
| sysroot_path=self.sysroot, | |
| packages=["package-1.0.0-r2"], | |
| ) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| def testSuccessOutputHandling(self): | |
| """Test successful call output handling.""" | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, sysroot_path=self.sysroot | |
| ) | |
| out_proto = self._OutputProto() | |
| self.PatchObject(sysroot_service, "BuildPackages") | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertFalse(rc) | |
| self.assertFalse(out_proto.failed_package_data) | |
| def testSuccessPackageIndexes(self): | |
| """Test successful call with package_indexes.""" | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| package_indexes = [ | |
| common_pb2.PackageIndexInfo( | |
| snapshot_sha="SHA", | |
| snapshot_number=5, | |
| build_target=common_pb2.BuildTarget(name="board"), | |
| location="LOCATION", | |
| profile=common_pb2.Profile(name="profile"), | |
| ), | |
| common_pb2.PackageIndexInfo( | |
| snapshot_sha="SHA2", | |
| snapshot_number=4, | |
| build_target=common_pb2.BuildTarget(name="board"), | |
| location="LOCATION2", | |
| profile=common_pb2.Profile(name="profile"), | |
| ), | |
| ] | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, | |
| sysroot_path=self.sysroot, | |
| package_indexes=package_indexes, | |
| ) | |
| out_proto = self._OutputProto() | |
| rc_patch = self.PatchObject(sysroot_service, "BuildPackagesRunConfig") | |
| self.PatchObject(sysroot_service, "BuildPackages") | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertFalse(rc) | |
| rc_patch.assert_called_with( | |
| use_any_chrome=False, | |
| usepkg=True, | |
| install_debug_symbols=True, | |
| packages=[], | |
| package_indexes=[ | |
| controller_util.deserialize_package_index_info(x) | |
| for x in package_indexes | |
| ], | |
| use_flags=[], | |
| use_goma=False, | |
| use_remoteexec=False, | |
| incremental_build=False, | |
| dryrun=False, | |
| backtrack=sysroot_controller.DEFAULT_BACKTRACK, | |
| workon=False, | |
| bazel=False, | |
| ) | |
| def testSuccessWithGomaLogs(self): | |
| """Test successful call with goma.""" | |
| self._CreateGomaLogFile( | |
| self.goma_dir, | |
| "compiler_proxy", | |
| datetime.datetime(2018, 9, 21, 12, 0, 0), | |
| ) | |
| self._CreateGomaLogFile( | |
| self.goma_dir, | |
| "compiler_proxy-subproc", | |
| datetime.datetime(2018, 9, 21, 12, 1, 0), | |
| ) | |
| self._CreateGomaLogFile( | |
| self.goma_dir, "gomacc", datetime.datetime(2018, 9, 21, 12, 2, 0) | |
| ) | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, | |
| sysroot_path=self.sysroot, | |
| goma_dir=self.goma_dir, | |
| goma_log_dir=self.goma_out_dir, | |
| ) | |
| out_proto = self._OutputProto() | |
| self.PatchObject(sysroot_service, "BuildPackages") | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertFalse(rc) | |
| self.assertFalse(out_proto.failed_package_data) | |
| expected = [ | |
| "compiler_proxy-subproc.host.log.INFO.20180921-120100.000000.gz", | |
| "compiler_proxy.host.log.INFO.20180921-120000.000000.gz", | |
| "gomacc.host.log.INFO.20180921-120200.000000.tar.gz", | |
| ] | |
| self.assertCountEqual(out_proto.goma_artifacts.log_files, expected) | |
| def testSuccessWithGomaLogsAndStatsCounterzFiles(self): | |
| """Test successful call with goma including stats and counterz files.""" | |
| self._CreateGomaLogFile( | |
| self.goma_dir, | |
| "compiler_proxy", | |
| datetime.datetime(2018, 9, 21, 12, 0, 0), | |
| ) | |
| self._CreateGomaLogFile( | |
| self.goma_dir, | |
| "compiler_proxy-subproc", | |
| datetime.datetime(2018, 9, 21, 12, 1, 0), | |
| ) | |
| self._CreateGomaLogFile( | |
| self.goma_dir, "gomacc", datetime.datetime(2018, 9, 21, 12, 2, 0) | |
| ) | |
| # Create stats and counterz files. | |
| osutils.WriteFile( | |
| os.path.join(self.goma_dir, "stats.binaryproto"), | |
| "File: stats.binaryproto", | |
| ) | |
| osutils.WriteFile( | |
| os.path.join(self.goma_dir, "counterz.binaryproto"), | |
| "File: counterz.binaryproto", | |
| ) | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, | |
| sysroot_path=self.sysroot, | |
| goma_dir=self.goma_dir, | |
| goma_log_dir=self.goma_out_dir, | |
| goma_stats_file="stats.binaryproto", | |
| goma_counterz_file="counterz.binaryproto", | |
| ) | |
| out_proto = self._OutputProto() | |
| self.PatchObject(sysroot_service, "BuildPackages") | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertFalse(rc) | |
| self.assertFalse(out_proto.failed_package_data) | |
| expected_logs = [ | |
| "compiler_proxy-subproc.host.log.INFO.20180921-120100.000000.gz", | |
| "compiler_proxy.host.log.INFO.20180921-120000.000000.gz", | |
| "gomacc.host.log.INFO.20180921-120200.000000.tar.gz", | |
| ] | |
| self.assertCountEqual(out_proto.goma_artifacts.log_files, expected_logs) | |
| # Verify that the output dir has 5 files -- since there should be 3 log | |
| # files, the stats file, and the counterz file. | |
| output_files = os.listdir(self.goma_out_dir) | |
| expected_output = [ | |
| "stats.binaryproto", | |
| "counterz.binaryproto", | |
| "compiler_proxy-subproc.host.log.INFO.20180921-120100.000000.gz", | |
| "compiler_proxy.host.log.INFO.20180921-120000.000000.gz", | |
| "gomacc.host.log.INFO.20180921-120200.000000.tar.gz", | |
| ] | |
| self.assertCountEqual(output_files, expected_output) | |
| self.assertEqual( | |
| out_proto.goma_artifacts.counterz_file, "counterz.binaryproto" | |
| ) | |
| self.assertEqual( | |
| out_proto.goma_artifacts.stats_file, "stats.binaryproto" | |
| ) | |
| def testFailureMissingGomaStatsCounterzFiles(self): | |
| """Test successful call with goma including stats and counterz files.""" | |
| self._CreateGomaLogFile( | |
| self.goma_dir, | |
| "compiler_proxy", | |
| datetime.datetime(2018, 9, 21, 12, 0, 0), | |
| ) | |
| self._CreateGomaLogFile( | |
| self.goma_dir, | |
| "compiler_proxy-subproc", | |
| datetime.datetime(2018, 9, 21, 12, 1, 0), | |
| ) | |
| self._CreateGomaLogFile( | |
| self.goma_dir, "gomacc", datetime.datetime(2018, 9, 21, 12, 2, 0) | |
| ) | |
| # Note that stats and counterz files are not created, but are specified | |
| # in the proto below. | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, | |
| sysroot_path=self.sysroot, | |
| goma_dir=self.goma_dir, | |
| goma_log_dir=self.goma_out_dir, | |
| goma_stats_file="stats.binaryproto", | |
| goma_counterz_file="counterz.binaryproto", | |
| ) | |
| out_proto = self._OutputProto() | |
| self.PatchObject(sysroot_service, "BuildPackages") | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| self.assertFalse(rc) | |
| self.assertFalse(out_proto.failed_package_data) | |
| expected_logs = [ | |
| "compiler_proxy-subproc.host.log.INFO.20180921-120100.000000.gz", | |
| "compiler_proxy.host.log.INFO.20180921-120000.000000.gz", | |
| "gomacc.host.log.INFO.20180921-120200.000000.tar.gz", | |
| ] | |
| self.assertCountEqual(out_proto.goma_artifacts.log_files, expected_logs) | |
| self.assertFalse(out_proto.goma_artifacts.counterz_file) | |
| self.assertFalse(out_proto.goma_artifacts.stats_file) | |
| def testFailureOutputHandling(self): | |
| """Test failed package handling.""" | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, sysroot_path=self.sysroot | |
| ) | |
| out_proto = self._OutputProto() | |
| # Failed package info and expected list for verification. | |
| err_pkgs = ["cat/pkg-1.0-r3", "cat2/pkg2-1.0-r1"] | |
| err_cpvs = [package_info.parse(cpv) for cpv in err_pkgs] | |
| expected = [("cat", "pkg"), ("cat2", "pkg2")] | |
| new_logs = {} | |
| for i, pkg in enumerate(err_pkgs): | |
| self._CreatePortageLogFile( | |
| self.portage_dir, | |
| err_cpvs[i], | |
| datetime.datetime(2021, 6, 9, 13, 37, 0), | |
| ) | |
| new_logs[pkg] = self._CreatePortageLogFile( | |
| self.portage_dir, | |
| err_cpvs[i], | |
| datetime.datetime(2021, 6, 9, 16, 20, 0), | |
| ) | |
| # Force error to be raised with the packages. | |
| error = sysroot_lib.PackageInstallError( | |
| "Error", cros_build_lib.CompletedProcess(), packages=err_cpvs | |
| ) | |
| self.PatchObject(sysroot_service, "BuildPackages", side_effect=error) | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # This needs to return 2 to indicate the available error response. | |
| self.assertEqual( | |
| controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc | |
| ) | |
| for data in out_proto.failed_package_data: | |
| package = controller_util.deserialize_package_info(data.name) | |
| cat_pkg = (data.name.category, data.name.package_name) | |
| self.assertIn(cat_pkg, expected) | |
| self.assertEqual(data.log_path.path, new_logs[package.cpvr]) | |
| def testNoPackageFailureOutputHandling(self): | |
| """Test failure handling without packages to report.""" | |
| # Prevent argument validation error. | |
| self.PatchObject( | |
| sysroot_lib.Sysroot, "IsToolchainInstalled", return_value=True | |
| ) | |
| in_proto = self._InputProto( | |
| build_target=self.build_target, sysroot_path=self.sysroot | |
| ) | |
| out_proto = self._OutputProto() | |
| # Force error to be raised with no packages. | |
| error = sysroot_lib.PackageInstallError( | |
| "Error", cros_build_lib.CompletedProcess(), packages=[] | |
| ) | |
| self.PatchObject(sysroot_service, "BuildPackages", side_effect=error) | |
| rc = sysroot_controller.InstallPackages( | |
| in_proto, out_proto, self.api_config | |
| ) | |
| # All we really care about is it's not 0 or 2 (response available), so | |
| # test for that rather than a specific return code. | |
| self.assertTrue(rc) | |
| self.assertNotEqual( | |
| controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc | |
| ) |