| # Copyright (C) 2023 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Integration tests for Kleaf. |
| |
| The rest of the arguments are passed to absltest. |
| |
| Example: |
| |
| tools/bazel run //build/kernel/kleaf/tests/integration_test |
| |
| tools/bazel run //build/kernel/kleaf/tests/integration_test \\ |
| -- --bazel-arg=--verbose_failures --bazel-arg=--announce_rc |
| |
| tools/bazel run //build/kernel/kleaf/tests/integration_test \\ |
| -- QuickIntegrationTest.test_menuconfig_merge |
| |
| tools/bazel run //build/kernel/kleaf/tests/integration_test \\ |
| -- --bazel-arg=--verbose_failures --bazel-arg=--announce_rc \\ |
| QuickIntegrationTest.test_menuconfig_merge \\ |
| --verbosity=2 |
| |
| tools/bazel run //build/kernel/kleaf/tests/integration_test \\ |
| -- --bazel-arg=--verbose_failures --include-abi-tests \\ |
| KleafIntegrationTestAbiTest.test_non_exported_symbol_fails |
| """ |
| |
| import argparse |
| import contextlib |
| import hashlib |
| import os |
| import re |
| import shlex |
| import shutil |
| import subprocess |
| import sys |
| import pathlib |
| import tempfile |
| import textwrap |
| import unittest |
| from typing import Any, Callable, Iterable, TextIO |
| |
| from absl.testing import absltest |
| from build.kernel.kleaf.analysis.inputs import analyze_inputs |
| |
| _BAZEL = pathlib.Path("tools/bazel") |
| |
| # See local.bazelrc |
| _LOCAL = ["--//build/kernel/kleaf:config_local"] |
| |
| _LTO_NONE = [ |
| "--lto=none", |
| "--nokmi_symbol_list_strict_mode", |
| ] |
| |
| # Handy arguments to build as fast as possible. |
| _FASTEST = _LOCAL + _LTO_NONE |
| |
| |
| def load_arguments(): |
| parser = argparse.ArgumentParser( |
| description=__doc__, formatter_class=argparse.RawTextHelpFormatter) |
| parser.add_argument("--bazel-arg", |
| action="append", |
| dest="bazel_args", |
| default=[], |
| help="arg to bazel build calls") |
| parser.add_argument("--bazel-wrapper-arg", |
| action="append", |
| dest="bazel_wrapper_args", |
| default=[], |
| help="arg to bazel.py wrapper") |
| parser.add_argument("--include-abi-tests", |
| action="store_true", |
| dest="include_abi_tests", |
| help="Include ABI Monitoring related tests." + |
| "NOTE: It requires a branch with ABI monitoring enabled.") |
| parser.add_argument("--mounted-kleaf-repo", |
| type=_require_absolute_path, |
| help="""The path to Kleaf tooling repository. |
| |
| If not set, some tests will mount the Kleaf |
| repository to certain locations and re-run itself |
| with the flag set.""") |
| group = parser.add_argument_group("CI", "flags for ci.android.com") |
| group.add_argument("--test_result_dir", |
| type=_require_absolute_path, |
| help="""Directory to store test results to be used in :reporter. |
| |
| If set, this script always has exit code 0. |
| """) |
| return parser.parse_known_args() |
| |
| |
| arguments = None |
| |
| |
| def _require_absolute_path(p: str) -> pathlib.Path: |
| path = pathlib.Path(p) |
| if not path.is_absolute(): |
| raise ValueError(f"{p} is not absolute") |
| return path |
| |
| |
| class Exec(object): |
| |
| @staticmethod |
| def check_call(args: list[str], **kwargs) -> None: |
| """Executes a shell command.""" |
| kwargs.setdefault("text", True) |
| sys.stderr.write(f"+ {' '.join(args)}\n") |
| subprocess.check_call(args, **kwargs) |
| |
| @staticmethod |
| def check_output(args: list[str], **kwargs) -> str: |
| """Returns output of a shell command""" |
| kwargs.setdefault("text", True) |
| sys.stderr.write(f"+ {' '.join(args)}\n") |
| return subprocess.check_output(args, **kwargs) |
| |
| @staticmethod |
| def popen(args: list[str], **kwargs) -> subprocess.Popen: |
| """Executes a shell command. |
| |
| Returns: |
| the Popen object |
| """ |
| kwargs.setdefault("text", True) |
| sys.stderr.write(f"+ {' '.join(args)}\n") |
| popen = subprocess.Popen(args, **kwargs) |
| return popen |
| |
| @staticmethod |
| def check_errors(args: list[str], **kwargs) -> str: |
| """Returns errors of a shell command""" |
| kwargs.setdefault("text", True) |
| sys.stderr.write(f"+ {' '.join(args)}\n") |
| return subprocess.run( |
| args, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs).stdout |
| |
| |
| class KleafIntegrationTestBase(unittest.TestCase): |
| |
| def _build_subprocess_args( |
| self, |
| command: str, |
| command_args: Iterable[str] = (), |
| use_bazelrc=True, |
| startup_options=(), |
| use_wrapper_args=True, |
| **kwargs, |
| ) -> tuple[list[str], dict[str, Any]]: |
| """Builds subprocess arguments.""" |
| subprocess_args = [str(_BAZEL)] |
| subprocess_args.extend(startup_options) |
| if use_bazelrc: |
| subprocess_args.append(f"--bazelrc={self._bazel_rc.name}") |
| subprocess_args.append(command) |
| |
| if "--" in command_args: |
| idx = command_args.index("--") |
| bazel_command_args = command_args[:idx] |
| script_args = command_args[idx:] |
| else: |
| bazel_command_args = command_args |
| script_args = [] |
| |
| subprocess_args.extend(bazel_command_args) |
| if use_wrapper_args: |
| subprocess_args.extend(arguments.bazel_wrapper_args) |
| subprocess_args.extend(script_args) |
| |
| # kwargs has known arguments filtered out. |
| return subprocess_args, kwargs |
| |
| def _check_call(self, *args, **kwargs) -> None: |
| """Executes a bazel command.""" |
| subprocess_args, kwargs = self._build_subprocess_args(*args, **kwargs) |
| Exec.check_call(subprocess_args, **kwargs) |
| |
| def _build(self, *args, **kwargs) -> None: |
| """Executes a bazel build command.""" |
| self._check_call("build", *args, **kwargs) |
| |
| def _check_output(self, *args, **kwargs) -> str: |
| """Returns output of a bazel command.""" |
| subprocess_args, kwargs = self._build_subprocess_args(*args, **kwargs) |
| return Exec.check_output(subprocess_args, **kwargs) |
| |
| def _check_errors(self, *args, **kwargs) -> str: |
| """Returns errors of a bazel command.""" |
| subprocess_args, kwargs = self._build_subprocess_args(*args, **kwargs) |
| return Exec.check_errors(subprocess_args, **kwargs) |
| |
| def _popen(self, *args, **kwargs) -> subprocess.Popen: |
| """Executes a bazel command, returning the Popen object.""" |
| subprocess_args, kwargs = self._build_subprocess_args(*args, **kwargs) |
| return Exec.popen(subprocess_args, **kwargs) |
| |
| def setUp(self) -> None: |
| self.assertTrue(os.environ.get("BUILD_WORKSPACE_DIRECTORY"), |
| "BUILD_WORKSPACE_DIRECTORY is not set. " + |
| "Did you use `tools/bazel test` instead of `tools/bazel run`?") |
| os.chdir(os.environ["BUILD_WORKSPACE_DIRECTORY"]) |
| sys.stderr.write( |
| f"BUILD_WORKSPACE_DIRECTORY={os.environ['BUILD_WORKSPACE_DIRECTORY']}\n" |
| ) |
| |
| self.assertTrue(_BAZEL.is_file()) |
| |
| self._bazel_rc = tempfile.NamedTemporaryFile() |
| self.addCleanup(self._bazel_rc.close) |
| with open(self._bazel_rc.name, "w") as f: |
| for arg in arguments.bazel_args: |
| f.write(f"build {shlex.quote(arg)}\n") |
| |
| def restore_file_after_test(self, path: pathlib.Path | str): |
| with open(path) as file: |
| old_content = file.read() |
| |
| def cleanup(): |
| with open(path, "w") as new_file: |
| new_file.write(old_content) |
| |
| self.addCleanup(cleanup) |
| return cleanup |
| |
| def filter_lines( |
| self, |
| path: pathlib.Path | str, |
| pred: Callable[[str], bool], |
| ): |
| """Filters lines in a file.""" |
| output_file_obj = tempfile.NamedTemporaryFile(mode="w", delete=False) |
| with open(path) as input_file: |
| with output_file_obj as output_file: |
| for line in input_file: |
| if pred(line): |
| output_file.write(line) |
| shutil.move(output_file.name, path) |
| |
| def replace_lines( |
| self, |
| path: pathlib.Path | str, |
| pred: Callable[[str], bool], |
| replacements: Iterable[str], |
| ): |
| """Replaces lines in a file.""" |
| output_file_obj = tempfile.NamedTemporaryFile(mode="w", delete=False) |
| it = iter(replacements) |
| with open(path) as input_file: |
| with output_file_obj as output_file: |
| for line in input_file: |
| if pred(line): |
| replaced_line = next(it) |
| output_file.write(replaced_line) |
| if not replaced_line.endswith("\n"): |
| output_file.write("\n") |
| else: |
| output_file.write(line) |
| shutil.move(output_file.name, path) |
| |
| def _sha256(self, path: pathlib.Path | str) -> str: |
| """Gets the hash for a file.""" |
| hash = hashlib.sha256() |
| with open(path, "rb") as file: |
| chunk = None |
| while chunk != b'': |
| chunk = file.read(4096) |
| hash.update(chunk) |
| return hash.hexdigest() |
| |
| def _touch(self, path: pathlib.Path | str, append_text="\n") -> None: |
| """Modifies a file so it (may) trigger a rebuild for certain targets.""" |
| self.restore_file_after_test(path) |
| |
| with open(path, "a") as file: |
| file.write(append_text) |
| |
| def _touch_core_kernel_file(self): |
| """Modifies a core kernel file.""" |
| self._touch(f"{self._common()}/kernel/sched/core.c") |
| |
| def _common(self) -> str: |
| """Returns the common package.""" |
| return "common" |
| |
| |
| # NOTE: It requires a branch with ABI monitoring enabled. |
| # Include these using the flag --include-abi-tests |
| class KleafIntegrationTestAbiTest(KleafIntegrationTestBase): |
| |
| def test_non_exported_symbol_fails(self): |
| """Tests the following: |
| |
| - Validates a non-exported symbol makes the build fail. |
| For this particular example use db845c mixed build. |
| |
| This test requires a branch with ABI monitoring enabled. |
| """ |
| |
| if not arguments.include_abi_tests: |
| self.skipTest("--include-abi-tests is not set.") |
| |
| # Select an arbitrary driver and unexport a symbols. |
| self.driver_file = f"{self._common()}/drivers/i2c/i2c-core-base.c" |
| self.restore_file_after_test(self.driver_file) |
| self.replace_lines(self.driver_file, |
| lambda x: re.search( |
| "EXPORT_SYMBOL_GPL\(i2c_adapter_type\);", x), |
| [""]) |
| |
| # Check for errors in the logs. |
| output = self._check_errors( |
| "build", [f"//{self._common()}:db845c", "--config=fast"]) |
| |
| def matching_line(line): return re.match( |
| r"^ERROR: modpost: \"i2c_adapter_type\" \[.*\] undefined!$", |
| line) |
| self.assertTrue( |
| any([matching_line(line) for line in output.splitlines()])) |
| |
| |
| # Slow integration tests belong to their own shard. |
| class KleafIntegrationTestShard1(KleafIntegrationTestBase): |
| |
| def test_incremental_switch_local_and_lto(self): |
| """Tests the following: |
| |
| - switching from non-local to local and back works |
| - with --config=local, changing from --lto=none to --lto=thin and back works |
| |
| See b/257288175.""" |
| self._build([f"//{self._common()}:kernel_dist"] + _LTO_NONE + _LOCAL) |
| self._build([f"//{self._common()}:kernel_dist"] + _LTO_NONE) |
| self._build([f"//{self._common()}:kernel_dist"] + _LTO_NONE + _LOCAL) |
| self._build([f"//{self._common()}:kernel_dist"] + |
| ["--lto=thin"] + _LOCAL) |
| self._build([f"//{self._common()}:kernel_dist"] + _LTO_NONE + _LOCAL) |
| |
| def test_config_sync(self): |
| """Test that, with --config=local, .config is reflected in vmlinux. |
| |
| See b/312268956. |
| """ |
| |
| gki_defconfig_path = ( |
| f"{self._common()}/arch/arm64/configs/gki_defconfig") |
| restore_defconfig = self.restore_file_after_test(gki_defconfig_path) |
| extract_ikconfig = f"{self._common()}/scripts/extract-ikconfig" |
| |
| with open(gki_defconfig_path, encoding="utf-8") as f: |
| self.assertIn("CONFIG_UAPI_HEADER_TEST=y\n", f) |
| |
| self._build([f"//{self._common()}:kernel_aarch64", "--config=fast"]) |
| vmlinux = pathlib.Path( |
| f"bazel-bin/{self._common()}/kernel_aarch64/vmlinux") |
| |
| output = subprocess.check_output([extract_ikconfig, vmlinux], text=True) |
| self.assertIn("CONFIG_UAPI_HEADER_TEST=y", output.splitlines()) |
| |
| self.filter_lines(gki_defconfig_path, |
| lambda x: "CONFIG_UAPI_HEADER_TEST" not in x) |
| self._build([f"//{self._common()}:kernel_aarch64", "--config=fast"]) |
| |
| output = subprocess.check_output([extract_ikconfig, vmlinux], text=True) |
| self.assertIn("# CONFIG_UAPI_HEADER_TEST is not set", |
| output.splitlines()) |
| |
| restore_defconfig() |
| self._build([f"//{self._common()}:kernel_aarch64", "--config=fast"]) |
| |
| output = subprocess.check_output([extract_ikconfig, vmlinux], text=True) |
| self.assertIn("CONFIG_UAPI_HEADER_TEST=y", output.splitlines()) |
| |
| |
| class KleafIntegrationTestShard2(KleafIntegrationTestBase): |
| |
| def test_user_clang_toolchain(self): |
| """Test --user_clang_toolchain option.""" |
| |
| clang_version = None |
| build_config_constants = f"{self._common()}/build.config.constants" |
| with open(build_config_constants) as f: |
| for line in f.read().splitlines(): |
| if line.startswith("CLANG_VERSION="): |
| clang_version = line.strip().split("=", 2)[1] |
| self.assertIsNotNone(clang_version) |
| clang_dir = f"prebuilts/clang/host/linux-x86/clang-{clang_version}" |
| clang_dir = os.path.realpath(clang_dir) |
| |
| # Do not use --config=local to ensure the toolchain dependency is |
| # correct. |
| args = [ |
| f"--user_clang_toolchain={clang_dir}", |
| f"//{self._common()}:kernel", |
| ] + _LTO_NONE |
| self._build(args) |
| |
| |
| class DdkWorkspaceSetupTest(KleafIntegrationTestBase): |
| """Tests setting up a DDK workspace with @kleaf as dependency.""" |
| |
| def setUp(self): |
| super().setUp() |
| |
| self.real_kleaf_repo = pathlib.Path(".").resolve() |
| self.ddk_workspace = (pathlib.Path(__file__).resolve().parent / |
| "ddk_workspace_test") |
| |
| def test_ddk_workspace_below_kleaf_module(self): |
| """Tests that DDK workspace is below @kleaf""" |
| self._run_ddk_workspace_setup_test(self.real_kleaf_repo) |
| |
| def test_kleaf_module_below_ddk_workspace(self): |
| """Tests that @kelaf is below DDK workspace""" |
| kleaf_repo = self.ddk_workspace / "external/kleaf" |
| this_test = self.id().removeprefix("__main__.") |
| if not arguments.mounted_kleaf_repo: |
| self._mount_and_run(kleaf_repo=kleaf_repo, test=this_test) |
| return |
| self._run_ddk_workspace_setup_test(arguments.mounted_kleaf_repo) |
| |
| def _mount_and_run(self, kleaf_repo: pathlib.Path, test: str): |
| args = [shutil.which("unshare"), "--mount", "--map-root-user"] |
| |
| # toybox unshare -R does not imply -U, so explicitly say so. |
| args.append("--user") |
| |
| args.extend([shutil.which("bash"), "-c"]) |
| |
| test_args = [sys.executable, __file__] |
| test_args.extend(f"--bazel-arg={arg}" for arg in arguments.bazel_args) |
| test_args.extend(f"--bazel-wrapper-arg={arg}" |
| for arg in arguments.bazel_wrapper_args) |
| test_args.append(f"--mounted-kleaf-repo={kleaf_repo}") |
| test_args.append(test) |
| args.append(" ".join(shlex.quote(str(test_arg)) |
| for test_arg in test_args)) |
| |
| Exec.check_call(args) |
| |
| def _run_ddk_workspace_setup_test(self, kleaf_repo: pathlib.Path): |
| # kleaf_repo relative to ddk_workspace |
| kleaf_repo_rel = self._force_relative_to( |
| kleaf_repo, self.ddk_workspace) |
| |
| git_clean_args = [shutil.which("git"), "clean", "-fdx"] |
| if kleaf_repo.is_relative_to(self.ddk_workspace): |
| git_clean_args.extend([ |
| "-e", |
| str(kleaf_repo.relative_to(self.ddk_workspace)), |
| ]) |
| |
| Exec.check_call(git_clean_args, cwd=self.ddk_workspace) |
| |
| # Delete generated files at the end |
| self.addCleanup(Exec.check_call, git_clean_args, |
| cwd=self.ddk_workspace) |
| |
| if kleaf_repo != self.real_kleaf_repo: |
| Exec.check_call([shutil.which("mount"), "--bind", "-o", "ro", |
| str(self.real_kleaf_repo), str(kleaf_repo)]) |
| self.addCleanup(Exec.check_call, |
| [shutil.which("umount"), str(kleaf_repo)]) |
| |
| self._check_call("run", [ |
| "//build/kernel:init_ddk", |
| "--", |
| "--local", |
| f"--kleaf_repo={kleaf_repo}", |
| f"--ddk_workspace={self.ddk_workspace}", |
| ]) |
| Exec.check_call([ |
| sys.executable, |
| str(self.ddk_workspace / "extra_setup.py"), |
| f"--kleaf_repo_rel={kleaf_repo_rel}", |
| f"--ddk_workspace={self.ddk_workspace}", |
| ]) |
| |
| self._check_call("clean", ["--expunge"], cwd=self.ddk_workspace) |
| self._check_call("test", ["//tests"], cwd=self.ddk_workspace) |
| |
| # Delete generated files |
| self._check_call("clean", ["--expunge"], cwd=self.ddk_workspace) |
| |
| @staticmethod |
| def _force_relative_to(path: pathlib.Path, other: pathlib.Path): |
| """Naive implementation of pathlib.Path.relative_to(walk_up)""" |
| if sys.version_info[0] == 3 and sys.version_info[1] >= 12: |
| return path.relative_to(other, walk_up=True) |
| |
| path = path.resolve() |
| other = other.resolve() |
| |
| if path.is_relative_to(other): |
| return path.relative_to(other) |
| |
| if (len(path.parts) <= len(other.parts) and |
| other.parts[:len(path.parts)] == path.parts): |
| parts = [".."] * (len(other.parts) - len(path.parts)) |
| return pathlib.Path(*parts) |
| raise ValueError( |
| f"Cannot calculate relative path from {path} to {other}") |
| |
| |
| # Quick integration tests. Each test case should finish within 1 minute. |
| # The whole test suite should finish within 5 minutes. If the whole test suite |
| # takes too long, consider sharding QuickIntegrationTest too. |
| |
| |
| class QuickIntegrationTest(KleafIntegrationTestBase): |
| |
| def test_change_to_core_kernel_does_not_affect_modules_prepare(self): |
| """Tests that, with a small change to the core kernel, modules_prepare does not change. |
| |
| See b/254357038, b/267385639, b/263415662. |
| """ |
| modules_prepare_archive = \ |
| f"bazel-bin/{self._common()}/kernel_aarch64_modules_prepare/modules_prepare_outdir.tar.gz" |
| |
| # This also tests that fixdep is not needed. |
| self._build([f"//{self._common()}:kernel_aarch64_modules_prepare"] + |
| _FASTEST) |
| first_hash = self._sha256(modules_prepare_archive) |
| |
| old_modules_archive = tempfile.NamedTemporaryFile(delete=False) |
| shutil.copyfile(modules_prepare_archive, old_modules_archive.name) |
| |
| self._touch_core_kernel_file() |
| |
| self._build([f"//{self._common()}:kernel_aarch64_modules_prepare"] + |
| _FASTEST) |
| second_hash = self._sha256(modules_prepare_archive) |
| |
| if first_hash == second_hash: |
| os.unlink(old_modules_archive.name) |
| |
| self.assertEqual( |
| first_hash, second_hash, |
| textwrap.dedent(f"""\ |
| Check their content here: |
| old: {old_modules_archive.name} |
| new: {modules_prepare_archive}""")) |
| |
| def test_module_does_not_depend_on_vmlinux(self): |
| """Tests that, the inputs for building a module does not include vmlinux and System.map. |
| |
| See b/254357038.""" |
| vd_modules = self._check_output("query", [ |
| 'kind("^_kernel_module rule$", //common-modules/virtual-device/...)' |
| ]).splitlines() |
| self.assertTrue(vd_modules) |
| |
| print( |
| f"+ build/kernel/kleaf/analysis/inputs.py 'mnemonic(\"KernelModule.*\", {vd_modules[0]})'" |
| ) |
| input_to_module = analyze_inputs( |
| aquery_args=[f'mnemonic("KernelModule.*", {vd_modules[0]})'] + |
| _FASTEST).keys() |
| self.assertFalse([ |
| path |
| for path in input_to_module if pathlib.Path(path).name == "vmlinux" |
| ], "An external module must not depend on vmlinux") |
| self.assertFalse([ |
| path for path in input_to_module |
| if pathlib.Path(path).name == "System.map" |
| ], "An external module must not depend on System.map") |
| |
| def test_override_javatmp(self): |
| """Tests that out/bazel/javatmp can be overridden. |
| |
| See b/267580482.""" |
| default_java_tmp = pathlib.Path("out/bazel/javatmp") |
| new_java_tmp = tempfile.TemporaryDirectory() |
| self.addCleanup(new_java_tmp.cleanup) |
| try: |
| shutil.rmtree(default_java_tmp) |
| except FileNotFoundError: |
| pass |
| self._check_call(startup_options=[ |
| f"--host_jvm_args=-Djava.io.tmpdir={new_java_tmp.name}" |
| ], |
| command="build", |
| command_args=["//build/kernel/kleaf:empty_test"] + |
| _FASTEST) |
| self.assertFalse(default_java_tmp.exists()) |
| |
| def test_override_absolute_out_dir(self): |
| """Tests that out/ can be overridden. |
| |
| See b/267580482.""" |
| new_out1 = tempfile.TemporaryDirectory() |
| new_out2 = tempfile.TemporaryDirectory() |
| self.addCleanup(new_out1.cleanup) |
| self.addCleanup(new_out2.cleanup) |
| shutil.rmtree(new_out1.name) |
| shutil.rmtree(new_out2.name) |
| |
| self._check_call(startup_options=[f"--output_root={new_out1.name}"], |
| command="build", |
| command_args=["//build/kernel/kleaf:empty_test"] + |
| _FASTEST) |
| self.assertTrue(pathlib.Path(new_out1.name).exists()) |
| self.assertFalse(pathlib.Path(new_out2.name).exists()) |
| shutil.rmtree(new_out1.name) |
| self._check_call(startup_options=[f"--output_root={new_out2.name}"], |
| command="build", |
| command_args=["//build/kernel/kleaf:empty_test"] + |
| _FASTEST) |
| self.assertFalse(pathlib.Path(new_out1.name).exists()) |
| self.assertTrue(pathlib.Path(new_out2.name).exists()) |
| |
| def test_config_uapi_header_test(self): |
| """Tests that CONFIG_UAPI_HEADER_TEST is not deleted. |
| |
| To keep CONFIG_UAPI_HEADER_TEST, USERCFLAGS needs to set --sysroot and |
| --target properly, and USERLDFLAGS needs to set --sysroot. |
| |
| See b/270996321 and b/190019968.""" |
| |
| archs = [ |
| ("aarch64", "arm64"), |
| ("x86_64", "x86"), |
| # TODO(b/271919464): Need NDK_TRIPLE for riscv so --sysroot is properly set |
| # ("riscv64", "riscv"), |
| ] |
| |
| for arch, srcarch in archs: |
| with self.subTest(arch=arch, srcarch=srcarch): |
| gki_defconfig = f"{self._common()}/arch/{srcarch}/configs/gki_defconfig" |
| self.restore_file_after_test(gki_defconfig) |
| |
| self._check_call("run", [ |
| f"//{self._common()}:kernel_{arch}_config", "--", |
| "olddefconfig" |
| ] + _FASTEST) |
| |
| with open(gki_defconfig) as f: |
| new_gki_defconfig_content = f.read() |
| self.assertTrue( |
| "CONFIG_UAPI_HEADER_TEST=y" |
| in new_gki_defconfig_content.splitlines(), |
| f"gki_defconfig should still have CONFIG_UAPI_HEADER_TEST=y after " |
| f"`bazel run //{self._common()}:kernel_aarch64_config " |
| f"-- olddefconfig`, but got\n{new_gki_defconfig_content}") |
| |
| # It should be fine to call the same command subsequently. |
| self._check_call("run", [ |
| f"//{self._common()}:kernel_{arch}_config", "--", |
| "olddefconfig" |
| ] + _FASTEST) |
| |
| def test_menuconfig_merge(self): |
| """Test that menuconfig works with a raw merge_config.sh in PRE_DEFCONFIG_CMDS. |
| |
| See `menuconfig_merge_test/` for details. |
| |
| See b/276889737 and b/274878805.""" |
| |
| args = [ |
| "//build/kernel/kleaf/tests/integration_test/menuconfig_merge_test:menuconfig_merge_test_config", |
| ] + _FASTEST |
| |
| output = self._check_output("run", args) |
| |
| def matching_line(line): return re.match( |
| r"^Updating .*common/arch/arm64/configs/menuconfig_test_defconfig$", |
| line) |
| self.assertTrue( |
| any([matching_line(line) for line in output.splitlines()])) |
| |
| # It should be fine to call the same command subsequently. |
| self._check_call("run", args) |
| |
| def test_menuconfig_fragment(self): |
| """Test that menuconfig works with a FRAGMENT_CONFIG defined. |
| |
| See `menuconfig_fragment_test/` for details. |
| |
| See b/276889737 and b/274878805.""" |
| |
| args = [ |
| "//build/kernel/kleaf/tests/integration_test/menuconfig_fragment_test:menuconfig_fragment_test_config", |
| ] + _FASTEST |
| |
| output = self._check_output("run", args) |
| |
| expected_line = f"Updated {os.environ['BUILD_WORKSPACE_DIRECTORY']}/build/kernel/kleaf/tests/integration_test/menuconfig_fragment_test/defconfig.fragment" |
| self.assertTrue(expected_line, output.splitlines()) |
| |
| # It should be fine to call the same command subsequently. |
| self._check_call("run", args) |
| |
| def test_ddk_defconfig_must_present(self): |
| """Test that for ddk_module, items in defconfig must be in the final .config. |
| |
| See b/279105294. |
| """ |
| |
| args = [ |
| "//build/kernel/kleaf/tests/integration_test/ddk_negative_test:defconfig_must_present_test_module_config", |
| ] + _FASTEST |
| popen = self._popen("build", |
| args, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| _, stderr = popen.communicate() |
| self.assertNotEqual(popen.returncode, 0) |
| self.assertIn( |
| "CONFIG_DELETED_SET: actual '', expected 'CONFIG_DELETED_SET=y' from build/kernel/kleaf/tests/integration_test/ddk_negative_test/defconfig.", |
| stderr) |
| self.assertNotIn("DECLARED_SET", stderr) |
| self.assertNotIn("DECLARED_UNSET", stderr) |
| |
| def test_dash_dash_help(self): |
| """Test that `bazel --help` works.""" |
| self._check_output("--help", use_bazelrc=False, use_wrapper_args=False) |
| |
| def test_help(self): |
| """Test that `bazel help` works.""" |
| self._check_output("help") |
| |
| def test_help_kleaf(self): |
| """Test that `bazel help kleaf` works.""" |
| self._check_output("help", ["kleaf"]) |
| |
| def test_strip_execroot(self): |
| """Test that --strip_execroot works.""" |
| self._check_output("build", ["--nobuild", "--strip_execroot", |
| "//build/kernel:hermetic-tools"]) |
| |
| def test_strip_execroot_error(self): |
| """Tests that if cmd with --strip_execroot fails, exit code is set.""" |
| popen = self._popen("what", |
| ["--strip_execroot"], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| popen.communicate() |
| self.assertNotEqual(popen.returncode, 0) |
| |
| def test_no_unexpected_output_error_with_proper_flags(self): |
| """With --config=silent, no errors emitted on unexpected lines.""" |
| |
| # Do a build with `--config=local` to force analysis cache to be invalid |
| # in the next run, which triggers |
| # WARNING: Build options [...] have changed, discarding analysis cache |
| self._build( |
| ["//build/kernel:hermetic-tools", "--config=local"], |
| ) |
| |
| allowlist = pathlib.Path("build/kernel/kleaf/spotless_log_regex.txt") |
| |
| startup_options = [ |
| f"--stdout_stderr_regex_allowlist={allowlist.resolve()}", |
| ] |
| self._build(["--config=silent", "//build/kernel:hermetic-tools"], |
| startup_options=startup_options) |
| |
| def test_detect_unexpected_output_error(self): |
| """Without --config=silent, there are errors on unexpected lines.""" |
| allowlist = pathlib.Path("build/kernel/kleaf/spotless_log_regex.txt") |
| |
| startup_options = [ |
| f"--stdout_stderr_regex_allowlist={allowlist.resolve()}", |
| ] |
| stderr = self._check_errors( |
| "build", |
| ["//build/kernel:hermetic-tools"], |
| startup_options=startup_options, |
| ) |
| self.assertIn("unexpected lines", stderr) |
| |
| def test_no_unexpected_output_error_if_process_exits_abnormally(self): |
| """If the bazel command fails, no errors emitted on unexpected lines.""" |
| allowlist = pathlib.Path("build/kernel/kleaf/spotless_log_regex.txt") |
| |
| startup_options = [ |
| f"--stdout_stderr_regex_allowlist={allowlist.resolve()}", |
| ] |
| stderr = self._check_errors( |
| "build", |
| ["//does_not_exist"], |
| startup_options=startup_options, |
| ) |
| self.assertNotIn("unexpected lines", stderr) |
| |
| class ScmversionIntegrationTest(KleafIntegrationTestBase): |
| |
| def setUp(self) -> None: |
| super().setUp() |
| |
| self.strings = shutil.which("llvm-strings") |
| self.uname_pattern_prefix = re.compile( |
| r"^Linux version [0-9]+[.][0-9]+[.][0-9]+(\S*)") |
| |
| self.build_config_common_path = f"{self._common()}/build.config.common" |
| self.restore_file_after_test(self.build_config_common_path) |
| |
| self.gki_defconfig_path = f"{self._common()}/arch/arm64/configs/gki_defconfig" |
| self.restore_file_after_test(self.gki_defconfig_path) |
| |
| self.makefile_path = f"{self._common()}/Makefile" |
| self.restore_file_after_test(self.makefile_path) |
| |
| def _setup_mainline(self): |
| with open(self.build_config_common_path, "a") as f: |
| f.write("BRANCH=android-mainline\n") |
| f.write("unset KMI_GENERATION\n") |
| |
| # Writing to defconfig directly requires us to disable check_defconfig, |
| # because the ordering is not correct. |
| self.build_config_gki_aarch64_path = f"{self._common()}/build.config.gki.aarch64" |
| self.restore_file_after_test(self.build_config_gki_aarch64_path) |
| with open(self.build_config_gki_aarch64_path, "a") as f: |
| f.write("POST_DEFCONFIG_CMDS=true\n") |
| |
| extraversion_pattern = re.compile(r"^EXTRAVERSION\s*=") |
| self.replace_lines(self.makefile_path, |
| lambda x: re.search(extraversion_pattern, x), |
| ["EXTRAVERSION = -rc999"]) |
| |
| def _setup_release_branch(self): |
| with open(self.build_config_common_path, "a") as f: |
| f.write( |
| textwrap.dedent("""\ |
| BRANCH=android99-100.110 |
| KMI_GENERATION=56 |
| """)) |
| |
| localversion_pattern = re.compile(r"^CONFIG_LOCALVERSION=") |
| self.filter_lines(self.gki_defconfig_path, |
| lambda x: not re.search(localversion_pattern, x)) |
| extraversion_pattern = re.compile(r"^EXTRAVERSION\s*=") |
| self.replace_lines(self.makefile_path, |
| lambda x: re.search(extraversion_pattern, x), |
| ["EXTRAVERSION ="]) |
| |
| def _get_vmlinux_scmversion(self): |
| strings_output = Exec.check_output([ |
| self.strings, f"bazel-bin/{self._common()}/kernel_aarch64/vmlinux" |
| ]) |
| ret = [] |
| for line in strings_output.splitlines(): |
| mo = re.search(self.uname_pattern_prefix, line) |
| if mo: |
| ret.append(mo.group(1)) |
| self.assertTrue(ret) |
| print(f"scmversion = {ret}") |
| return ret |
| |
| @staticmethod |
| def _env_without_build_number(): |
| env = dict(os.environ) |
| env.pop("BUILD_NUMBER", None) |
| # Fix this error to execute `repo` properly: |
| # ModuleNotFoundError: No module named 'color' |
| env.pop("PYTHONSAFEPATH", None) |
| return env |
| |
| @staticmethod |
| def _env_with_build_number(build_number): |
| env = dict(os.environ) |
| env["BUILD_NUMBER"] = str(build_number) |
| # Fix this error to execute `repo` properly: |
| # ModuleNotFoundError: No module named 'color' |
| env.pop("PYTHONSAFEPATH", None) |
| return env |
| |
| def test_mainline_no_stamp(self): |
| self._setup_mainline() |
| self._check_call( |
| "build", |
| _FASTEST + [ |
| "--config=local", |
| f"//{self._common()}:kernel_aarch64", |
| ], |
| env=ScmversionIntegrationTest._env_without_build_number()) |
| for scmversion in self._get_vmlinux_scmversion(): |
| self.assertEqual("-rc999-mainline-maybe-dirty", scmversion) |
| |
| def test_mainline_stamp(self): |
| self._setup_mainline() |
| self._check_call( |
| "build", |
| _FASTEST + [ |
| "--config=stamp", |
| "--config=local", |
| f"//{self._common()}:kernel_aarch64", |
| ], |
| env=ScmversionIntegrationTest._env_without_build_number()) |
| scmversion_pat = re.compile( |
| r"^-rc999-mainline(-[0-9]{5,})?-g[0-9a-f]{12,40}(-dirty)?$") |
| for scmversion in self._get_vmlinux_scmversion(): |
| self.assertRegexpMatches(scmversion, scmversion_pat) |
| |
| def test_mainline_ab(self): |
| self._setup_mainline() |
| self._check_call( |
| "build", |
| _FASTEST + [ |
| "--config=stamp", |
| "--config=local", |
| f"//{self._common()}:kernel_aarch64", |
| ], |
| env=ScmversionIntegrationTest._env_with_build_number("123456")) |
| scmversion_pat = re.compile( |
| r"^-rc999-mainline(-[0-9]{5,})?-g[0-9a-f]{12,40}(-dirty)?-ab123456$" |
| ) |
| for scmversion in self._get_vmlinux_scmversion(): |
| self.assertRegexpMatches(scmversion, scmversion_pat) |
| |
| def test_release_branch_no_stamp(self): |
| self._setup_release_branch() |
| self._check_call( |
| "build", |
| _FASTEST + [ |
| "--config=local", |
| f"//{self._common()}:kernel_aarch64", |
| ], |
| env=ScmversionIntegrationTest._env_without_build_number()) |
| for scmversion in self._get_vmlinux_scmversion(): |
| self.assertEqual("-android99-56-maybe-dirty", scmversion) |
| |
| def test_release_branch_stamp(self): |
| self._setup_release_branch() |
| self._check_call( |
| "build", |
| _FASTEST + [ |
| "--config=stamp", |
| "--config=local", |
| f"//{self._common()}:kernel_aarch64", |
| ], |
| env=ScmversionIntegrationTest._env_without_build_number()) |
| scmversion_pat = re.compile( |
| r"^-android99-56(-[0-9]{5,})?-g[0-9a-f]{12,40}(-dirty)?$") |
| for scmversion in self._get_vmlinux_scmversion(): |
| self.assertRegexpMatches(scmversion, scmversion_pat) |
| |
| def test_release_branch_ab(self): |
| self._setup_release_branch() |
| self._check_call( |
| "build", |
| _FASTEST + [ |
| "--config=stamp", |
| "--config=local", |
| f"//{self._common()}:kernel_aarch64", |
| ], |
| env=ScmversionIntegrationTest._env_with_build_number("123456")) |
| scmversion_pat = re.compile( |
| r"^-android99-56(-[0-9]{5,})?-g[0-9a-f]{12,40}(-dirty)?-ab123456$") |
| for scmversion in self._get_vmlinux_scmversion(): |
| self.assertRegexpMatches(scmversion, scmversion_pat) |
| |
| |
| # Class that mimics tee(1) |
| class Tee(object): |
| def __init__(self, stream: TextIO, path: pathlib.Path): |
| self._stream = stream |
| self._path = path |
| |
| def __getattr__(self, name: str) -> Any: |
| return getattr(self._stream, name) |
| |
| def write(self, *args, **kwargs) -> int: |
| # Ignore short write to console |
| self._stream.write(*args, **kwargs) |
| return self._file.write(*args, **kwargs) |
| |
| def __enter__(self) -> "Tee": |
| self._file = open(self._path, "w") |
| return self |
| |
| def __exit__(self, exc_type, exc_val, exc_tb): |
| self._file.close() |
| |
| |
| def _get_exit_code(exc: SystemExit): |
| if exc.code is None: |
| return 0 |
| # absltest calls sys.exit() with a boolean value. |
| if type(exc.code) == bool: |
| return int(exc.code) |
| if type(exc.code) == int: |
| return exc.code |
| print( |
| f"ERROR: Unknown exit code: {e.code}, exiting with code 1", |
| file=sys.stderr) |
| return 1 |
| |
| |
| if __name__ == "__main__": |
| arguments, unknown = load_arguments() |
| |
| if not arguments.test_result_dir: |
| sys.argv[1:] = unknown |
| absltest.main() |
| sys.exit(0) |
| |
| # If --test_result_dir is set, also set --xml_output_file for :reporter. |
| unknown += [ |
| "--xml_output_file", |
| str(arguments.test_result_dir / "output.xml") |
| ] |
| sys.argv[1:] = unknown |
| |
| os.makedirs(arguments.test_result_dir, exist_ok=True) |
| stdout_path = arguments.test_result_dir / "stdout.txt" |
| stderr_path = arguments.test_result_dir / "stderr.txt" |
| with Tee(sys.__stdout__, stdout_path) as stdout_tee, \ |
| Tee(sys.__stderr__, stderr_path) as stderr_tee, \ |
| contextlib.redirect_stdout(stdout_tee), \ |
| contextlib.redirect_stderr(stderr_tee): |
| try: |
| absltest.main() |
| exit_code = 0 |
| except SystemExit as e: |
| exit_code = _get_exit_code(e) |
| |
| exit_code_path = arguments.test_result_dir / "exitcode.txt" |
| with open(exit_code_path, "w") as exit_code_file: |
| exit_code_file.write(f"{exit_code}\n") |