• 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 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