blob: 07590b57d1596137ce89b6d2e8aadf2afd992369 [file] [log] [blame] [edit]
# 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 Optional, Union
from unittest import mock
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.api.gen.chromiumos import prebuilts_cloud_pb2
from chromite.lib import build_query
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
MOCK_BINHOST_LOOKUP_SERVICE_DATA = prebuilts_cloud_pb2.BinhostLookupServiceData(
snapshot_shas=[], private=False
)
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,
lookup_service_data=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 lookup_service_data:
proto.binhost_lookup_service_data.CopyFrom(lookup_service_data)
return proto
def _OutputProto(self):
"""Helper to build output proto instance."""
return sysroot_pb2.SysrootCreateResponse()
def testValidateOnly(self) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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,
use_cq_prebuilts=use_cq_prebuilts,
backtrack=sysroot_controller.DEFAULT_BACKTRACK,
binhost_lookup_service_data=MOCK_BINHOST_LOOKUP_SERVICE_DATA,
)
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
binhost_data = prebuilts_cloud_pb2.BinhostLookupServiceData(
snapshot_shas=["deadbeef", "beefdead"], private=True
)
in_proto = self._InputProto(
build_target=board,
profile=profile,
replace=force,
current=not upgrade_chroot,
use_cq_prebuilts=use_cq_prebuilts,
lookup_service_data=binhost_data,
)
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,
upgrade_chroot=upgrade_chroot,
use_cq_prebuilts=use_cq_prebuilts,
backtrack=sysroot_controller.DEFAULT_BACKTRACK,
binhost_lookup_service_data=binhost_data,
)
self.assertEqual(board, out_proto.sysroot.build_target.name)
self.assertEqual(profile, out_proto.sysroot.build_target.profile.name)
self.assertEqual(sysroot_path, out_proto.sysroot.path)
class GetTargetArchitectureTest(
cros_test_lib.MockTestCase, api_config.ApiConfigMixin
):
"""Tests for SysrootService/GetTargetArchitecture()."""
def setUp(self) -> None:
"""Basic patching to make the tests hermetic."""
self.PatchObject(
build_query.Board,
"get",
return_value=build_query.Board("some-board"),
)
def _patch_board_arch(self, return_value: Optional[str]) -> None:
"""Patch the return value of build_query.Board.arch."""
self.PatchObject(
build_query.Board,
"arch",
return_value=return_value,
new_callable=mock.PropertyMock,
)
def test_basic(self) -> None:
"""Test a basic successful call to GetTargetArchitecture()."""
self._patch_board_arch("amd64")
request = sysroot_pb2.GetTargetArchitectureRequest(
build_target=common_pb2.BuildTarget(name="some-board")
)
response = sysroot_pb2.GetTargetArchitectureResponse()
sysroot_controller.GetTargetArchitecture(
request,
response,
self.api_config,
)
self.assertEqual(response.architecture, "amd64")
def test_no_build_target_name(self) -> None:
"""Test a request with no build target name."""
request = sysroot_pb2.GetTargetArchitectureRequest()
response = sysroot_pb2.GetTargetArchitectureResponse()
with self.assertRaises(cros_build_lib.DieSystemExit):
sysroot_controller.GetTargetArchitecture(
request,
response,
self.api_config,
)
def test_arch_is_none(self) -> None:
"""Test a call where the target architecture returns None."""
self._patch_board_arch(None)
request = sysroot_pb2.GetTargetArchitectureRequest(
build_target=common_pb2.BuildTarget(name="some-board")
)
response = sysroot_pb2.GetTargetArchitectureResponse()
sysroot_controller.GetTargetArchitecture(
request,
response,
self.api_config,
)
self.assertEqual(response.architecture, "")
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,
common_pb2.ArtifactsByService.Sysroot.ArtifactType.BAZEL_PERFORMANCE_ARTIFACTS: sysroot_service.CollectBazelPerformanceArtifacts,
}
# pylint: enable=line-too-long
def setUp(self) -> None:
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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
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) -> None:
"""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) -> None:
"""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) -> None:
"""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 ExtractArchiveTest(
cros_test_lib.MockTempDirTestCase, api_config.ApiConfigMixin
):
"""ExtractArchive function tests."""
def setUp(self) -> None:
self.chroot_path = "/path/to/chroot"
self.board = "board"
self.sysroot_archive = "/path/to/archive"
def _InputProto(
self, build_target=None, chroot_path=None, sysroot_archive=None
):
"""Helper to build and input proto instance."""
return sysroot_pb2.SysrootExtractArchiveRequest(
build_target={"name": build_target},
chroot={"path": chroot_path},
sysroot_archive={
"path": sysroot_archive,
"location": common_pb2.Path.Location.OUTSIDE,
},
)
def _OutputProto(self):
"""Helper to build output proto instance."""
return sysroot_pb2.SysrootExtractArchiveResponse()
def testValidateOnly(self) -> None:
"""Verify a validate-only call does not execute any logic."""
patch = self.PatchObject(sysroot_service, "ExtractSysroot")
in_proto = self._InputProto(
build_target=self.board,
chroot_path=self.chroot_path,
sysroot_archive=self.sysroot_archive,
)
with self.assertRaises(cros_build_lib.DieSystemExit):
sysroot_controller.ExtractArchive(
in_proto, self._OutputProto(), self.validate_only_config
)
patch.assert_not_called()
def testMockCall(self) -> None:
"""Sanity check that a mock call does not execute any logic."""
patch = self.PatchObject(sysroot_service, "ExtractSysroot")
in_proto = self._InputProto(
build_target=self.board,
chroot_path=self.chroot_path,
sysroot_archive=self.sysroot_archive,
)
sysroot_controller.ExtractArchive(
in_proto, self._OutputProto(), self.mock_call_config
)
patch.assert_not_called()
def testArgumentValidation(self) -> None:
"""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.ExtractArchive(
in_proto, out_proto, self.api_config
)
# Error when sysroot_archive is not existed.
in_proto = self._InputProto(
build_target="board",
chroot_path=self.chroot_path,
sysroot_archive=self.sysroot_archive,
)
with self.assertRaises(cros_build_lib.DieSystemExit):
sysroot_controller.ExtractArchive(
in_proto, out_proto, self.api_config
)
# Valid when board, chroot path, and sysroot_archive are specified.
tmpfile = self.tempdir / "tmp.tar.zst"
osutils.Touch(tmpfile)
patch = self.PatchObject(
sysroot_service,
"ExtractSysroot",
return_value=self.chroot_path,
)
in_proto = self._InputProto(
build_target="board",
chroot_path=self.chroot_path,
sysroot_archive=str(tmpfile),
)
out_proto = self._OutputProto()
sysroot_controller.ExtractArchive(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) -> None:
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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
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)
def _InputProto(
self,
build_target=None,
sysroot_path=None,
build_source=False,
use_cq_prebuilts=False,
packages=None,
bazel=False,
binhost_lookup_service_data=MOCK_BINHOST_LOOKUP_SERVICE_DATA,
):
"""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 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
if binhost_lookup_service_data:
instance.binhost_lookup_service_data.CopyFrom(
binhost_lookup_service_data
)
return instance
def _OutputProto(self):
"""Helper to build an empty output proto instance."""
return sysroot_pb2.InstallPackagesResponse()
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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
"""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) -> None:
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) -> None:
"""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 testFailureOutputHandling(self) -> None:
"""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) -> None:
"""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
)
def testReclientBuild(self) -> None:
"""Test successful reclient build."""
# 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,
)
cfg_file_name = "reproxy_release.cfg"
in_proto.remoteexec_config.reproxy_cfg_file = cfg_file_name
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,
packages=[],
use_flags=[],
use_remoteexec=True,
reproxy_cfg_file=cfg_file_name,
incremental_build=False,
dryrun=False,
backtrack=sysroot_controller.DEFAULT_BACKTRACK,
workon=False,
bazel=False,
bazel_lite=False,
noclean=False,
binhost_lookup_service_data=MOCK_BINHOST_LOOKUP_SERVICE_DATA,
timeout=None,
bazel_use_remote_execution=False,
)