1#!/usr/bin/env python3 2# Copyright 2023 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import copy 7import os 8from pathlib import Path 9import sys 10from typing import Any, Iterable, List, Optional, Union 11from impl.common import ( 12 CROSVM_ROOT, 13 TOOLS_ROOT, 14 Command, 15 Remote, 16 quoted, 17 Styles, 18 argh, 19 console, 20 chdir, 21 cmd, 22 record_time, 23 run_main, 24 sudo_is_passwordless, 25 verbose, 26 Triple, 27) 28from impl.test_config import ROOT_TESTS, DO_NOT_RUN, DO_NOT_RUN_AARCH64, DO_NOT_RUN_WIN64, E2E_TESTS 29from impl import testvm 30 31rsync = cmd("rsync") 32cargo = cmd("cargo") 33 34# Name of the directory used to package all test files. 35PACKAGE_NAME = "integration_tests_package" 36 37 38def join_filters(items: Iterable[str], op: str): 39 return op.join(f"({i})" for i in items) 40 41 42class TestFilter(object): 43 """ 44 Utility structure to join user-provided filter expressions with additional filters 45 46 See https://nexte.st/book/filter-expressions.html 47 """ 48 49 def __init__(self, expression: str): 50 self.expression = expression 51 52 def exclude(self, *exclude_exprs: str): 53 return self.subset(f"not ({join_filters(exclude_exprs, '|')})") 54 55 def include(self, *include_exprs: str): 56 include_expr = join_filters(include_exprs, "|") 57 return TestFilter(f"({self.expression}) | ({include_expr})") 58 59 def subset(self, *subset_exprs: str): 60 subset_expr = join_filters(subset_exprs, "|") 61 if not self.expression: 62 return TestFilter(subset_expr) 63 return TestFilter(f"({self.expression}) & ({subset_expr})") 64 65 def to_args(self): 66 if not self.expression: 67 return 68 yield "--filter-expr" 69 yield quoted(self.expression) 70 71 72def configure_cargo(cmd: Command, triple: Triple, features: str): 73 "Configures the provided cmd with cargo arguments and environment needed to build for triple." 74 return ( 75 cmd.with_args( 76 "--workspace", 77 f"--features={features}", 78 ) 79 .with_color_flag() 80 .with_envs(triple.get_cargo_env()) 81 ) 82 83 84class HostTarget(object): 85 def __init__(self, package_dir: Path): 86 self.run_cmd = cmd(package_dir / "run.sh").with_color_flag() 87 88 def run_tests(self, extra_args: List[Any]): 89 return self.run_cmd.with_args(*extra_args).fg(style=Styles.live_truncated(), check=False) 90 91 92class SshTarget(object): 93 def __init__(self, package_archive: Path, remote: Remote): 94 console.print("Transfering integration tests package...") 95 with record_time("Transfering"): 96 remote.scp([package_archive], "") 97 with record_time("Unpacking"): 98 remote.ssh(cmd("tar xaf", package_archive.name)).fg(style=Styles.live_truncated()) 99 self.remote_run_cmd = cmd(f"{PACKAGE_NAME}/run.sh").with_color_flag() 100 self.remote = remote 101 102 def run_tests(self, extra_args: List[Any]): 103 return self.remote.ssh(self.remote_run_cmd.with_args(*extra_args)).fg( 104 style=Styles.live_truncated(), 105 check=False, 106 ) 107 108 109def check_host_prerequisites(run_root_tests: bool): 110 "Check various prerequisites for executing test binaries." 111 if os.name == "nt": 112 return 113 114 if run_root_tests: 115 console.print("Running tests that require root privileges. Refreshing sudo now.") 116 cmd("sudo true").fg() 117 118 for device in ["/dev/kvm", "/dev/vhost-vsock"]: 119 if not os.access(device, os.R_OK | os.W_OK): 120 console.print(f"{device} access is required", style="red") 121 sys.exit(1) 122 123 124def check_build_prerequisites(triple: Triple): 125 installed_toolchains = cmd("rustup target list --installed").lines() 126 if str(triple) not in installed_toolchains: 127 console.print(f"Your host is not configured to build for [green]{triple}[/green]") 128 console.print(f"[green]Tip:[/green] Run tests in the dev container with:") 129 console.print() 130 console.print( 131 f" [blue]$ tools/dev_container tools/run_tests {' '.join(sys.argv[1:])}[/blue]" 132 ) 133 sys.exit(1) 134 135 136def get_vm_arch(triple: Triple): 137 if str(triple) == "x86_64-unknown-linux-gnu": 138 return "x86_64" 139 elif str(triple) == "aarch64-unknown-linux-gnu": 140 return "aarch64" 141 else: 142 raise Exception(f"{triple} is not supported for running tests in a VM.") 143 144 145@argh.arg("--filter-expr", "-E", type=str, action="append", help="Nextest filter expression.") 146@argh.arg("--platform", "-p", help="Which platform to test. (x86_64, aarch64, armhw, mingw64)") 147@argh.arg("--dut", help="Which device to test on. (vm or host)") 148@argh.arg("--no-run", "--build-only", help="Build only, do not run any tests.") 149@argh.arg("--no-unit-tests", help="Do not run unit tests.") 150@argh.arg("--no-integration-tests", help="Do not run integration tests.") 151@argh.arg("--no-strip", help="Do not strip test binaries of debug info.") 152@argh.arg("--run-root-tests", help="Enables integration tests that require root privileges.") 153@argh.arg( 154 "--features", 155 help=f"List of comma separated features to be passed to cargo. Defaults to `all-$platform`", 156) 157@argh.arg("--profile", help="Select nextest profile.") 158@argh.arg("--no-parallel", help="Do not parallelize integration tests. Slower but more stable.") 159@argh.arg("--repetitions", help="Repeat all tests, useful for checking test stability.") 160def main( 161 filter_expr: List[str] = [], 162 platform: Optional[str] = None, 163 dut: Optional[str] = None, 164 no_run: bool = False, 165 no_unit_tests: bool = False, 166 no_integration_tests: bool = False, 167 no_strip: bool = False, 168 run_root_tests: bool = False, 169 features: Optional[str] = None, 170 profile: Optional[str] = None, 171 no_parallel: bool = False, 172 repetitions: int = 1, 173): 174 """ 175 Runs all crosvm tests 176 177 For details on how crosvm tests are organized, see https://crosvm.dev/book/testing.html 178 179 # Basic Usage 180 181 To run all unit tests for the hosts native architecture: 182 183 $ ./tools/run_tests2 184 185 To run all unit tests for another supported architecture using an emulator (e.g. wine64, 186 qemu user space emulation). 187 188 $ ./tools/run_tests2 -p aarch64 189 $ ./tools/run_tests2 -p armhw 190 $ ./tools/run_tests2 -p mingw64 191 192 # Integration Tests 193 194 Integration tests can be run on a built-in virtual machine: 195 196 $ ./tools/run_tests2 --dut=vm 197 $ ./tools/run_tests2 --dut=vm -p aarch64 198 199 The virtual machine is automatically started for the test process and can be managed via the 200 `./tools/x86vm` or `./tools/aarch64vm` tools. 201 202 Integration tests can be run on the host machine as well, but cannot be guaranteed to work on 203 all configurations. 204 205 $ ./tools/run_tests2 --dut=host 206 207 # Test Filtering 208 209 This script supports nextest filter expressions: https://nexte.st/book/filter-expressions.html 210 211 For example to run all tests in `my-crate` and all crates that depend on it: 212 213 $ ./tools/run_tests2 [--dut=] -E 'rdeps(my-crate)' 214 """ 215 chdir(CROSVM_ROOT) 216 217 if os.name == "posix" and not cmd("which cargo-nextest").success(): 218 raise Exception("Cannot find cargo-nextest. Please re-run `./tools/install-deps`") 219 elif os.name == "nt" and not cmd("where.exe cargo-nextest.exe").success(): 220 raise Exception("Cannot find cargo-nextest. Please re-run `./tools/install-deps.ps1`") 221 222 triple = Triple.from_shorthand(platform) if platform else Triple.host_default() 223 224 test_filter = TestFilter(join_filters(filter_expr, "|")) 225 226 if not features: 227 features = triple.feature_flag 228 229 if no_run: 230 no_integration_tests = True 231 no_unit_tests = True 232 233 # Disable the DUT if integration tests are not run. 234 if no_integration_tests: 235 dut = None 236 237 # Automatically enable tests that require root if sudo is passwordless 238 if not run_root_tests: 239 if dut == "host": 240 run_root_tests = sudo_is_passwordless() 241 elif dut == "vm": 242 # The test VMs have passwordless sudo configured. 243 run_root_tests = True 244 245 # Print summary of tests and where they will be executed. 246 if dut == "host": 247 dut_str = "Run on host" 248 elif dut == "vm" and os.name == "posix": 249 dut_str = f"Run on built-in {get_vm_arch(triple)} vm" 250 elif dut == None: 251 dut_str = "[yellow]Skip[/yellow]" 252 else: 253 raise Exception( 254 f"--dut={dut} is not supported. Options are --dut=host or --dut=vm (linux only)" 255 ) 256 257 features = features if features else triple.feature_flag 258 skip_str = "[yellow]skip[/yellow]" 259 unit_test_str = "Run on host" if not no_unit_tests else skip_str 260 integration_test_str = dut_str if dut else skip_str 261 console.print(f"Running tests for [green]{triple}[/green]") 262 console.print(f"With features: [green]{features}[/green]") 263 console.print() 264 console.print(f" Unit tests: [bold]{unit_test_str}[/bold]") 265 console.print(f" Integration tests: [bold]{integration_test_str}[/bold]") 266 console.print() 267 268 check_build_prerequisites(triple) 269 270 # Print tips in certain configurations. 271 if dut and not run_root_tests: 272 console.print( 273 "[green]Tip:[/green] Skipping tests that require root privileges. " 274 + "Use [bold]--run-root-tests[/bold] to enable them." 275 ) 276 if not dut: 277 console.print( 278 "[green]Tip:[/green] To run integration tests on a built-in VM: " 279 + "Use [bold]--dut=vm[/bold]" 280 ) 281 console.print( 282 "[green]Tip:[/green] To run integration tests on the host: Use " 283 + "[bold]--dut=host[/bold]" 284 ) 285 286 # Prepare the dut for test execution 287 if dut == "host": 288 check_host_prerequisites(run_root_tests) 289 if dut == "vm": 290 testvm.build_if_needed(get_vm_arch(triple)) 291 testvm.up(get_vm_arch(triple)) 292 293 nextest_args = [ 294 f"--profile={profile}" if profile else None, 295 "--verbose" if verbose() else None, 296 ] 297 298 console.print() 299 console.rule("Building tests") 300 nextest_run = configure_cargo(cmd("cargo nextest run"), triple, features).with_args( 301 *nextest_args 302 ) 303 with record_time("Build"): 304 returncode = nextest_run.with_args("--no-run").fg( 305 style=Styles.live_truncated(), check=False 306 ) 307 if returncode != 0: 308 sys.exit(returncode) 309 310 if not no_unit_tests: 311 unit_test_filter = copy.deepcopy(test_filter) 312 unit_test_filter.include("kind(bench)") 313 unit_test_filter.exclude(*E2E_TESTS) 314 console.print() 315 console.rule("Running unit tests") 316 with record_time("Unit Tests"): 317 for i in range(repetitions): 318 if repetitions > 1: 319 console.rule(f"Round {i}", style="grey") 320 321 returncode = nextest_run.with_args("--lib --bins", *unit_test_filter.to_args()).fg( 322 style=Styles.live_truncated(), check=False 323 ) 324 if returncode != 0: 325 sys.exit(returncode) 326 327 if dut: 328 package_dir = triple.target_dir / PACKAGE_NAME 329 package_archive = package_dir.with_suffix(".tar.zst") 330 nextest_package = configure_cargo(cmd(TOOLS_ROOT / "nextest_package"), triple, features) 331 332 test_exclusions = [*DO_NOT_RUN] 333 if not run_root_tests: 334 test_exclusions += ROOT_TESTS 335 if triple == Triple.from_shorthand("mingw64"): 336 test_exclusions += DO_NOT_RUN_WIN64 337 if triple == Triple.from_shorthand("aarch64"): 338 test_exclusions += DO_NOT_RUN_AARCH64 339 test_filter = test_filter.exclude(*test_exclusions) 340 341 console.print() 342 console.rule("Packaging integration tests") 343 with record_time("Packing"): 344 nextest_package( 345 "--test *", 346 f"-d {package_dir}", 347 f"-o {package_archive}" if dut != "host" else None, 348 "--no-strip" if no_strip else None, 349 *test_filter.to_args(), 350 "--verbose" if verbose() else None, 351 ).fg(style=Styles.live_truncated()) 352 353 target: Union[HostTarget, SshTarget] 354 if dut == "host": 355 target = HostTarget(package_dir) 356 elif dut == "vm": 357 testvm.wait(get_vm_arch(triple)) 358 remote = Remote("localhost", testvm.ssh_opts(get_vm_arch(triple))) 359 target = SshTarget(package_archive, remote) 360 361 console.print() 362 console.rule("Running integration tests") 363 with record_time("Integration tests"): 364 for i in range(repetitions): 365 if repetitions > 1: 366 console.rule(f"Round {i}", style="grey") 367 returncode = target.run_tests( 368 [ 369 *test_filter.to_args(), 370 *nextest_args, 371 "--test-threads=1" if no_parallel else None, 372 ] 373 ) 374 if returncode != 0: 375 if not no_parallel: 376 console.print( 377 "[green]Tip:[/green] Tests may fail when run in parallel on some platforms. " 378 + "Try re-running with `--no-parallel`" 379 ) 380 sys.exit(returncode) 381 382 383if __name__ == "__main__": 384 run_main(main) 385