run_tests 15 KB

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