#!/usr/bin/env python3 # Copyright 2021 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # # Runs tests for crosvm. # # This script gives us more flexibility in running tests than using # `cargo test --workspace`: # - We can also test crates that are not part of the workspace. # - We can pick out tests that need to be run single-threaded. # - We can filter out tests that cannot be built or run due to missing build- # dependencies or missing runtime requirements. import argparse import os import platform import subprocess from typing import List, Dict, Set import enum class Requirements(enum.Enum): # Test can only be built for aarch64 AARCH64 = "aarch64" # Test can only be built for x86_64 X86_64 = "x86_64" # Test requires non-standard dependencies from chromiumos (i.e. those that are # not available in debian buster). CROS_DEPENDENCIES = "cros_dependencies" # Test is disabled explicitly DISABLED = "disabled" # Test requires access to kernel devices at runtime. DEVICE_ACCESS = "device_access" # Test needs to be executed natively, not through emulation. NATIVE = "native" # Test needs to run single-threaded SINGLE_THREADED = "single_threaded" BUILD_TIME_REQUIREMENTS = [ Requirements.AARCH64, Requirements.X86_64, Requirements.CROS_DEPENDENCIES, Requirements.DISABLED, ] RUN_TIME_REQUIREMENTS = [Requirements.DEVICE_ACCESS, Requirements.NATIVE] # A list of all crates and their requirements CRATE_REQUIREMENTS: Dict[str, List[Requirements]] = { "crosvm": [Requirements.DEVICE_ACCESS], "aarch64": [Requirements.AARCH64], "acpi_tables": [], "arch": [], "assertions": [], "base": [], "bit_field": [], "bit_field_derive": [], "cros_async": [Requirements.DISABLED], "crosvm_plugin": [Requirements.X86_64], "data_model": [], "devices": [Requirements.SINGLE_THREADED, Requirements.DEVICE_ACCESS], "disk": [Requirements.DISABLED], "enumn": [], "fuse": [], "fuzz": [Requirements.DISABLED], "gpu_buffer": [Requirements.CROS_DEPENDENCIES], "gpu_display": [], "hypervisor": [Requirements.DEVICE_ACCESS], "io_uring": [Requirements.DISABLED], "kernel_cmdline": [], "kernel_loader": [Requirements.NATIVE], "kvm_sys": [Requirements.DEVICE_ACCESS], "kvm": [Requirements.DEVICE_ACCESS], "linux_input_sys": [], "msg_socket": [Requirements.NATIVE], "msg_on_socket_derive": [], "net_sys": [], "net_util": [Requirements.DEVICE_ACCESS], "power_monitor": [], "protos": [], "qcow_utils": [], "rand_ish": [], "resources": [], "rutabaga_gfx": [Requirements.CROS_DEPENDENCIES], "sync": [], "sys_util": [Requirements.SINGLE_THREADED, Requirements.NATIVE], "poll_token_derive": [], "syscall_defines": [], "tempfile": [], "tpm2-sys": [], "tpm2": [], "usb_sys": [], "usb_util": [], "vfio_sys": [], "vhost": [Requirements.DEVICE_ACCESS], "virtio_sys": [], "vm_control": [], "vm_memory": [Requirements.DISABLED], "x86_64": [Requirements.X86_64, Requirements.DEVICE_ACCESS], } # Just like for crates, lists requirements for each cargo feature flag. FEATURE_REQUIREMENTS: Dict[str, List[Requirements]] = { "chromeos": [Requirements.CROS_DEPENDENCIES], "audio": [], "gpu": [Requirements.CROS_DEPENDENCIES], "plugin": [Requirements.DEVICE_ACCESS, Requirements.X86_64], "power-monitor-powerd": [], "tpm": [Requirements.CROS_DEPENDENCIES], "video-decoder": [Requirements.DISABLED], "video-encoder": [Requirements.DISABLED], "wl-dmabuf": [Requirements.CROS_DEPENDENCIES, Requirements.DISABLED], "x": [], "virgl_renderer_next": [Requirements.CROS_DEPENDENCIES], "composite-disk": [], "virgl_renderer": [Requirements.CROS_DEPENDENCIES], "gfxstream": [Requirements.CROS_DEPENDENCIES, Requirements.DISABLED], "gdb": [], } def target_arch(): """Returns architecture cargo is set up to build for.""" if "CARGO_BUILD_TARGET" in os.environ: target = os.environ["CARGO_BUILD_TARGET"] return target.split("-")[0] else: return platform.machine() def is_native(): """True if we are building for the architecture we are running on.""" return target_arch() == platform.machine() class CrateInfo(object): """Informaton about whether a crate can be built or run on this host.""" def __init__( self, name: str, requirements: Set[Requirements], capabilities: Set[Requirements], ): self.name = name self.requirements = requirements self.single_threaded = Requirements.SINGLE_THREADED in requirements build_reqs = requirements.intersection(BUILD_TIME_REQUIREMENTS) self.can_build = all(req in capabilities for req in build_reqs) run_reqs = requirements.intersection(RUN_TIME_REQUIREMENTS) self.can_run = self.can_build and all( req in capabilities for req in run_reqs ) def execute_tests( crates: List[CrateInfo], features: Set[str], run: bool = True, single_threaded: bool = False, ): """Executes the list of crates via `cargo test`.""" if not crates: return True cmd = ["cargo", "test", "-q"] if not run: cmd += ["--no-run"] if features: cmd += ["--no-default-features", "--features", ",".join(features)] for crate in sorted(crate.name for crate in crates): cmd += ["-p", crate] if single_threaded: cmd += ["--", "--test-threads=1"] print("$", " ".join(cmd)) process = subprocess.run(cmd) return process.returncode == 0 def execute_test_batches(crates: List[CrateInfo], features: Set[str]): """Groups tests and runs them in 3 batches: - Those that can only be built, but not run. - Those that can only be run single-threaded. - And those that can be run in parallel. """ passed = True build_crates = [ crate for crate in crates if crate.can_build and not crate.can_run ] if not execute_tests(build_crates, features, run=False): passed = False run_single = [ crate for crate in crates if crate.can_run and crate.single_threaded ] if not execute_tests(run_single, features, single_threaded=True): passed = False run_parallel = [ crate for crate in crates if crate.can_run and not crate.single_threaded ] if not execute_tests(run_parallel, features): passed = False return passed def main(capabilities: Set[Requirements]): if target_arch() == "aarch64": capabilities.add(Requirements.AARCH64) elif target_arch() == "x86_64": capabilities.add(Requirements.X86_64) if is_native(): capabilities.add(Requirements.NATIVE) else: capabilities.remove(Requirements.DEVICE_ACCESS) print("Capabilities:", ", ".join(cap.value for cap in capabilities)) # Select all features where capabilities meet the requirements features = set( feature for (feature, requirements) in FEATURE_REQUIREMENTS.items() if all(r in capabilities for r in requirements) ) # Disable sandboxing for tests until our builders are set up to run with # sandboxing. features.add("default-no-sandbox") print("Features:", ", ".join(features)) crates = [ CrateInfo(crate, set(requirements), capabilities) for (crate, requirements) in CRATE_REQUIREMENTS.items() ] passed = execute_test_batches(crates, features) # TODO: We should parse test output and summarize the results # Unfortunately machine readable output for `cargo test` is still a nightly # rust feature. print() crates_not_built = [crate.name for crate in crates if not crate.can_build] print(f"Tests not built: {', '.join(crates_not_built)}") crates_not_run = [ crate.name for crate in crates if crate.can_build and not crate.can_run ] print(f"Tests not run: {', '.join(crates_not_run)}") disabled_features = set(FEATURE_REQUIREMENTS.keys()).difference(features) print(f"Disabled features: {', '.join(disabled_features)}") print() if not passed: print("Some tests failed.") exit(-1) else: print("All tests passed.") DESCRIPTION = """\ Selects a subset of tests from crosvm to run depending on the capabilities of the local host. """ if __name__ == "__main__": parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument( "--all", action="store_true", default=False, help="Enable all tests." ) args = parser.parse_args() main( set([Requirements.DEVICE_ACCESS, Requirements.CROS_DEPENDENCIES]) if args.all else set() )