123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- #!/usr/bin/env python3
- # Copyright 2023 The ChromiumOS Authors
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- import copy
- import os
- from pathlib import Path
- import sys
- from typing import Any, Iterable, List, Optional, Union
- from impl.common import (
- CROSVM_ROOT,
- TOOLS_ROOT,
- Command,
- Remote,
- quoted,
- Styles,
- argh,
- console,
- chdir,
- cmd,
- record_time,
- run_main,
- sudo_is_passwordless,
- verbose,
- Triple,
- )
- from impl.test_config import ROOT_TESTS, DO_NOT_RUN, DO_NOT_RUN_AARCH64, DO_NOT_RUN_WIN64, E2E_TESTS
- from impl.test_config import DO_NOT_BUILD_RISCV64, DO_NOT_RUN_WINE64
- from impl import testvm
- rsync = cmd("rsync")
- cargo = cmd("cargo")
- # Name of the directory used to package all test files.
- PACKAGE_NAME = "integration_tests_package"
- def join_filters(items: Iterable[str], op: str):
- return op.join(f"({i})" for i in items)
- class TestFilter(object):
- """
- Utility structure to join user-provided filter expressions with additional filters
- See https://nexte.st/book/filter-expressions.html
- """
- def __init__(self, expression: str):
- self.expression = expression
- def exclude(self, *exclude_exprs: str):
- return self.subset(f"not ({join_filters(exclude_exprs, '|')})")
- def include(self, *include_exprs: str):
- include_expr = join_filters(include_exprs, "|")
- return TestFilter(f"({self.expression}) | ({include_expr})")
- def subset(self, *subset_exprs: str):
- subset_expr = join_filters(subset_exprs, "|")
- if not self.expression:
- return TestFilter(subset_expr)
- return TestFilter(f"({self.expression}) & ({subset_expr})")
- def to_args(self):
- if not self.expression:
- return
- yield "--filter-expr"
- yield quoted(self.expression)
- def configure_cargo(
- cmd: Command, triple: Triple, features: Optional[str], no_default_features: bool
- ):
- "Configures the provided cmd with cargo arguments and environment needed to build for triple."
- return (
- cmd.with_args(
- "--workspace",
- "--no-default-features" if no_default_features else None,
- f"--features={features}" if features else None,
- )
- .with_color_flag()
- .with_envs(triple.get_cargo_env())
- )
- class HostTarget(object):
- def __init__(self, package_dir: Path):
- self.run_cmd = cmd(package_dir / "run.sh").with_color_flag()
- def run_tests(self, extra_args: List[Any]):
- return self.run_cmd.with_args(*extra_args).fg(style=Styles.live_truncated(), check=False)
- class SshTarget(object):
- def __init__(self, package_archive: Path, remote: Remote):
- console.print("Transfering integration tests package...")
- with record_time("Transfering"):
- remote.scp([package_archive], "")
- with record_time("Unpacking"):
- remote.ssh(cmd("tar xaf", package_archive.name)).fg(style=Styles.live_truncated())
- self.remote_run_cmd = cmd(f"{PACKAGE_NAME}/run.sh").with_color_flag()
- self.remote = remote
- def run_tests(self, extra_args: List[Any]):
- return self.remote.ssh(self.remote_run_cmd.with_args(*extra_args)).fg(
- style=Styles.live_truncated(),
- check=False,
- )
- def check_host_prerequisites(run_root_tests: bool):
- "Check various prerequisites for executing test binaries."
- if os.name == "nt":
- return
- if run_root_tests:
- console.print("Running tests that require root privileges. Refreshing sudo now.")
- cmd("sudo true").fg()
- for device in ["/dev/kvm", "/dev/vhost-vsock"]:
- if not os.access(device, os.R_OK | os.W_OK):
- console.print(f"{device} access is required", style="red")
- sys.exit(1)
- def check_build_prerequisites(triple: Triple):
- installed_toolchains = cmd("rustup target list --installed").lines()
- if str(triple) not in installed_toolchains:
- console.print(f"Your host is not configured to build for [green]{triple}[/green]")
- console.print(f"[green]Tip:[/green] Run tests in the dev container with:")
- console.print()
- console.print(
- f" [blue]$ tools/dev_container tools/run_tests {' '.join(sys.argv[1:])}[/blue]"
- )
- sys.exit(1)
- def get_vm_arch(triple: Triple):
- if str(triple) == "x86_64-unknown-linux-gnu":
- return "x86_64"
- elif str(triple) == "aarch64-unknown-linux-gnu":
- return "aarch64"
- elif str(triple) == "riscv64gc-unknown-linux-gnu":
- return "riscv64"
- else:
- raise Exception(f"{triple} is not supported for running tests in a VM.")
- @argh.arg("--filter-expr", "-E", type=str, action="append", help="Nextest filter expression.")
- @argh.arg(
- "--platform", "-p", help="Which platform to test. (x86_64, aarch64, armhw, mingw64, riscv64)"
- )
- @argh.arg("--dut", help="Which device to test on. (vm or host)")
- @argh.arg("--no-default-features", help="Don't enable default features")
- @argh.arg("--no-run", "--build-only", help="Build only, do not run any tests.")
- @argh.arg("--no-unit-tests", help="Do not run unit tests.")
- @argh.arg("--no-integration-tests", help="Do not run integration tests.")
- @argh.arg("--no-strip", help="Do not strip test binaries of debug info.")
- @argh.arg("--run-root-tests", help="Enables integration tests that require root privileges.")
- @argh.arg(
- "--features",
- help=f"List of comma separated features to be passed to cargo. Defaults to `all-$platform`",
- )
- @argh.arg("--no-parallel", help="Do not parallelize integration tests. Slower but more stable.")
- @argh.arg("--repetitions", help="Repeat all tests, useful for checking test stability.")
- def main(
- filter_expr: List[str] = [],
- platform: Optional[str] = None,
- dut: Optional[str] = None,
- no_default_features: bool = False,
- no_run: bool = False,
- no_unit_tests: bool = False,
- no_integration_tests: bool = False,
- no_strip: bool = False,
- run_root_tests: bool = False,
- features: Optional[str] = None,
- no_parallel: bool = False,
- repetitions: int = 1,
- ):
- """
- Runs all crosvm tests
- For details on how crosvm tests are organized, see https://crosvm.dev/book/testing/index.html
- # Basic Usage
- To run all unit tests for the hosts native architecture:
- $ ./tools/run_tests
- To run all unit tests for another supported architecture using an emulator (e.g. wine64,
- qemu user space emulation).
- $ ./tools/run_tests -p aarch64
- $ ./tools/run_tests -p armhw
- $ ./tools/run_tests -p mingw64
- # Integration Tests
- Integration tests can be run on a built-in virtual machine:
- $ ./tools/run_tests --dut=vm
- $ ./tools/run_tests --dut=vm -p aarch64
- The virtual machine is automatically started for the test process and can be managed via the
- `./tools/x86vm` or `./tools/aarch64vm` tools.
- Integration tests can be run on the host machine as well, but cannot be guaranteed to work on
- all configurations.
- $ ./tools/run_tests --dut=host
- # Test Filtering
- This script supports nextest filter expressions: https://nexte.st/book/filter-expressions.html
- For example to run all tests in `my-crate` and all crates that depend on it:
- $ ./tools/run_tests [--dut=] -E 'rdeps(my-crate)'
- """
- chdir(CROSVM_ROOT)
- if os.name == "posix" and not cmd("which cargo-nextest").success():
- raise Exception("Cannot find cargo-nextest. Please re-run `./tools/install-deps`")
- elif os.name == "nt" and not cmd("where.exe cargo-nextest.exe").success():
- raise Exception("Cannot find cargo-nextest. Please re-run `./tools/install-deps.ps1`")
- triple = Triple.from_shorthand(platform) if platform else Triple.host_default()
- test_filter = TestFilter(join_filters(filter_expr, "|"))
- if not features and not no_default_features:
- features = triple.feature_flag
- if no_run:
- no_integration_tests = True
- no_unit_tests = True
- # Disable the DUT if integration tests are not run.
- if no_integration_tests:
- dut = None
- # Automatically enable tests that require root if sudo is passwordless
- if not run_root_tests:
- if dut == "host":
- run_root_tests = sudo_is_passwordless()
- elif dut == "vm":
- # The test VMs have passwordless sudo configured.
- run_root_tests = True
- # Print summary of tests and where they will be executed.
- if dut == "host":
- dut_str = "Run on host"
- elif dut == "vm" and os.name == "posix":
- dut_str = f"Run on built-in {get_vm_arch(triple)} vm"
- elif dut == None:
- dut_str = "[yellow]Skip[/yellow]"
- else:
- raise Exception(
- f"--dut={dut} is not supported. Options are --dut=host or --dut=vm (linux only)"
- )
- skip_str = "[yellow]skip[/yellow]"
- unit_test_str = "Run on host" if not no_unit_tests else skip_str
- integration_test_str = dut_str if dut else skip_str
- profile = os.environ.get("NEXTEST_PROFILE", "default")
- console.print(f"Running tests for [green]{triple}[/green]")
- console.print(f"Profile: [green]{profile}[/green]")
- console.print(f"With features: [green]{features}[/green]")
- console.print(f"no-default-features: [green]{no_default_features}[/green]")
- console.print()
- console.print(f" Unit tests: [bold]{unit_test_str}[/bold]")
- console.print(f" Integration tests: [bold]{integration_test_str}[/bold]")
- console.print()
- check_build_prerequisites(triple)
- # Print tips in certain configurations.
- if dut and not run_root_tests:
- console.print(
- "[green]Tip:[/green] Skipping tests that require root privileges. "
- + "Use [bold]--run-root-tests[/bold] to enable them."
- )
- if not dut:
- console.print(
- "[green]Tip:[/green] To run integration tests on a built-in VM: "
- + "Use [bold]--dut=vm[/bold] (preferred)"
- )
- console.print(
- "[green]Tip:[/green] To run integration tests on the host: Use "
- + "[bold]--dut=host[/bold] (fast, but unreliable)"
- )
- if dut == "vm":
- vm_arch = get_vm_arch(triple)
- if vm_arch == "x86_64":
- cli_tool = "tools/x86vm"
- elif vm_arch == "aarch64":
- cli_tool = "tools/aarch64vm"
- else:
- raise Exception(f"Unknown vm arch '{vm_arch}'")
- console.print(
- f"[green]Tip:[/green] The test VM will remain alive between tests. You can manage this VM with [bold]{cli_tool}[/bold]"
- )
- # Prepare the dut for test execution
- if dut == "host":
- check_host_prerequisites(run_root_tests)
- if dut == "vm":
- # Start VM ahead of time but don't wait for it to boot.
- testvm.up(get_vm_arch(triple))
- nextest_args = [
- f"--profile={profile}" if profile else None,
- "--verbose" if verbose() else None,
- ]
- console.print()
- console.rule("Building tests")
- if triple == Triple.from_shorthand("riscv64"):
- nextest_args += ["--exclude=" + s for s in DO_NOT_BUILD_RISCV64]
- nextest_run = configure_cargo(
- cmd("cargo nextest run"), triple, features, no_default_features
- ).with_args(*nextest_args)
- with record_time("Build"):
- returncode = nextest_run.with_args("--no-run").fg(
- style=Styles.live_truncated(), check=False
- )
- if returncode != 0:
- sys.exit(returncode)
- if not no_unit_tests:
- unit_test_filter = copy.deepcopy(test_filter).exclude(*E2E_TESTS).include("kind(bench)")
- if triple == Triple.from_shorthand("mingw64") and os.name == "posix":
- unit_test_filter = unit_test_filter.exclude(*DO_NOT_RUN_WINE64)
- console.print()
- console.rule("Running unit tests")
- with record_time("Unit Tests"):
- for i in range(repetitions):
- if repetitions > 1:
- console.rule(f"Round {i}", style="grey")
- returncode = nextest_run.with_args("--lib --bins", *unit_test_filter.to_args()).fg(
- style=Styles.live_truncated(), check=False
- )
- if returncode != 0:
- sys.exit(returncode)
- if dut:
- package_dir = triple.target_dir / PACKAGE_NAME
- package_archive = package_dir.with_suffix(".tar.zst")
- nextest_package = configure_cargo(
- cmd(TOOLS_ROOT / "nextest_package"), triple, features, no_default_features
- )
- test_exclusions = [*DO_NOT_RUN]
- if not run_root_tests:
- test_exclusions += ROOT_TESTS
- if triple == Triple.from_shorthand("mingw64"):
- test_exclusions += DO_NOT_RUN_WIN64
- if os.name == "posix":
- test_exclusions += DO_NOT_RUN_WINE64
- if triple == Triple.from_shorthand("aarch64"):
- test_exclusions += DO_NOT_RUN_AARCH64
- test_filter = test_filter.exclude(*test_exclusions)
- console.print()
- console.rule("Packaging integration tests")
- with record_time("Packing"):
- nextest_package(
- "--test *",
- f"-d {package_dir}",
- f"-o {package_archive}" if dut != "host" else None,
- "--no-strip" if no_strip else None,
- *test_filter.to_args(),
- "--verbose" if verbose() else None,
- ).fg(style=Styles.live_truncated())
- target: Union[HostTarget, SshTarget]
- if dut == "host":
- target = HostTarget(package_dir)
- elif dut == "vm":
- testvm.up(get_vm_arch(triple), wait=True)
- remote = Remote("localhost", testvm.ssh_opts(get_vm_arch(triple)))
- target = SshTarget(package_archive, remote)
- console.print()
- console.rule("Running integration tests")
- with record_time("Integration tests"):
- for i in range(repetitions):
- if repetitions > 1:
- console.rule(f"Round {i}", style="grey")
- returncode = target.run_tests(
- [
- *test_filter.to_args(),
- *nextest_args,
- "--test-threads=1" if no_parallel else None,
- ]
- )
- if returncode != 0:
- if not no_parallel:
- console.print(
- "[green]Tip:[/green] Tests may fail when run in parallel on some platforms. "
- + "Try re-running with `--no-parallel`"
- )
- if dut == "host":
- console.print(
- f"[yellow]Tip:[/yellow] Running tests on the host may not be reliable. "
- "Prefer [bold]--dut=vm[/bold]."
- )
- sys.exit(returncode)
- if __name__ == "__main__":
- run_main(main)
|