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