blob: de6a290f927c6090751d2cf55ef2db20b2a51eee [file] [log] [blame]
#!/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()
)