mirror of
https://chromium.googlesource.com/crosvm/crosvm
synced 2025-02-11 12:35:26 +00:00
Updates run_tests to use cargo style target triples for specifying build targets. A simple 'aarch64' or 'armhf' was nice while we just had linux builds. We now are looking at windows and possibly different toolchain options (e.g. msvc vs gnu), so our old system was getting confusing and inconsistent. We used to have some special handling for adding wrappers to test runs for emulation (e.g. wine, qemu). That logic has been moved into TestTarget which now contains not just where to run the test but also how. Supported are armhf/aarch64 qemu as well as wine64. The CLI has been updated to match and now uses the build-target argument instead of arch. The following combinations have been tested (though not all combinations actually pass all tests, which is a separate issue). ./tools/run_tests ./tools/run_tests --target=host --build-target=x86_64-unknown-linux-gnu ./tools/run_tests --target=host --build-target=armhf ./tools/run_tests --target=host --build-target=aarch64 ./tools/run_tests --target=host --build-target=mingw64 ./tools/run_tests --target=vm:aarch64 ./tools/run_tests --target=vm:aarch64 --build-target=armhf BUG=b:233914170 TEST=See above Change-Id: Ic6dbb5b39788e2573714606d3bb0e7c712032d91 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/3739240 Tested-by: kokoro <noreply+kokoro@google.com> Commit-Queue: Dennis Kempin <denniskempin@google.com> Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
468 lines
15 KiB
Python
Executable file
468 lines
15 KiB
Python
Executable file
# Copyright 2021 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file
|
|
import argparse
|
|
import functools
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any, Literal, Optional, cast, List, Dict, NamedTuple
|
|
import sys
|
|
from . import testvm
|
|
import os
|
|
|
|
USAGE = """Choose to run tests locally, in a vm or on a remote machine.
|
|
|
|
To set the default test target to run on one of the build-in VMs:
|
|
|
|
./tools/set_test_target vm:aarch64 && source .envrc
|
|
|
|
Then as usual run cargo or run_tests:
|
|
|
|
./tools/run_tests
|
|
cargo test
|
|
|
|
The command will prepare the VM for testing (e.g. upload required shared
|
|
libraries for running rust tests) and set up run_tests as well as cargo
|
|
to build for the test target and execute tests on it.
|
|
|
|
Arbitrary SSH remotes can be used for running tests as well. e.g.
|
|
|
|
./tools/set_test_target ssh:remotehost
|
|
|
|
The `remotehost` needs to be properly configured for passwordless
|
|
authentication.
|
|
|
|
Tip: Use http://direnv.net to automatically load the envrc file instead of
|
|
having to source it after each call.
|
|
"""
|
|
|
|
SCRIPT_PATH = Path(__file__).resolve()
|
|
SCRIPT_DIR = SCRIPT_PATH.parent.resolve()
|
|
TESTVM_DIR = SCRIPT_DIR.parent.joinpath("testvm")
|
|
TARGET_DIR = testvm.cargo_target_dir().joinpath("crosvm_tools")
|
|
ENVRC_PATH = SCRIPT_DIR.parent.parent.joinpath(".envrc")
|
|
|
|
Arch = Literal["x86_64", "aarch64", "armhf", "win64"]
|
|
|
|
# Enviroment variables needed for building with cargo
|
|
BUILD_ENV = {
|
|
"PKG_CONFIG_armv7_unknown_linux_gnueabihf": "arm-linux-gnueabihf-pkg-config",
|
|
}
|
|
|
|
if platform.machine() != "aarch64":
|
|
BUILD_ENV.update(
|
|
{
|
|
"PKG_CONFIG_aarch64_unknown_linux_gnu": "aarch64-linux-gnu-pkg-config",
|
|
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "aarch64-linux-gnu-gcc",
|
|
}
|
|
)
|
|
|
|
|
|
class Ssh:
|
|
"""Wrapper around subprocess to execute commands remotely via SSH."""
|
|
|
|
hostname: str
|
|
opts: List[str]
|
|
|
|
def __init__(self, hostname: str, opts: List[str] = []):
|
|
self.hostname = hostname
|
|
self.opts = opts
|
|
|
|
def run(self, cmd: str, **kwargs: Any):
|
|
"""Equivalent of subprocess.run"""
|
|
return subprocess.run(
|
|
[
|
|
"ssh",
|
|
self.hostname,
|
|
*self.opts,
|
|
# Do not create a tty. This will mess with terminal output
|
|
# when running multiple subprocesses.
|
|
"-T",
|
|
# Tell sh to kill children on hangup.
|
|
f"shopt -s huponexit; {cmd}",
|
|
],
|
|
**kwargs,
|
|
)
|
|
|
|
def check_output(self, cmd: str):
|
|
"""Equivalent of subprocess.check_output"""
|
|
return subprocess.run(
|
|
["ssh", self.hostname, *self.opts, "-T", cmd],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
check=True,
|
|
).stdout
|
|
|
|
def upload_files(self, files: List[Path], remote_dir: str = "", quiet: bool = False):
|
|
"""Wrapper around SCP."""
|
|
flags: List[str] = []
|
|
if quiet:
|
|
flags.append("-q")
|
|
scp_cmd = [
|
|
"scp",
|
|
*flags,
|
|
*self.opts,
|
|
*(str(f) for f in files),
|
|
f"{self.hostname}:{remote_dir}",
|
|
]
|
|
subprocess.run(scp_cmd, check=True)
|
|
|
|
|
|
class Triple(NamedTuple):
|
|
"""
|
|
Build triple in cargo format.
|
|
|
|
The format is: <arch><sub>-<vendor>-<sys>-<abi>, However, we will treat <arch><sub> as a single
|
|
arch to simplify things.
|
|
"""
|
|
|
|
arch: str
|
|
vendor: str
|
|
sys: Optional[str]
|
|
abi: Optional[str]
|
|
|
|
@classmethod
|
|
def from_shorthand(cls, shorthand: str):
|
|
"These shorthands make it easier to specify triples on the command line."
|
|
if "-" in shorthand:
|
|
triple = shorthand
|
|
elif shorthand == "mingw64":
|
|
triple = "x86_64-pc-windows-gnu"
|
|
elif shorthand == "msvc64":
|
|
triple = "x86_64-pc-windows-msvc"
|
|
elif shorthand == "armhf":
|
|
triple = "armv7-unknown-linux-gnueabihf"
|
|
elif shorthand == "aarch64":
|
|
triple = "aarch64-unknown-linux-gnu"
|
|
elif shorthand == "x86_64":
|
|
triple = "x86_64-unknown-linux-gnu"
|
|
else:
|
|
raise Exception("Not a valid build triple shorthand: {shorthand}")
|
|
return cls.from_str(triple)
|
|
|
|
@classmethod
|
|
def from_str(cls, triple: str):
|
|
parts = triple.split("-")
|
|
if len(parts) < 2:
|
|
raise Exception(f"Unsupported triple {triple}")
|
|
return cls(
|
|
parts[0],
|
|
parts[1],
|
|
parts[2] if len(parts) > 2 else None,
|
|
parts[3] if len(parts) > 3 else None,
|
|
)
|
|
|
|
@classmethod
|
|
def from_linux_arch(cls, arch: str):
|
|
"Rough logic to convert the output of `arch` into a corresponding linux build triple."
|
|
if arch == "armhf":
|
|
return cls.from_str("armv7-unknown-linux-gnueabihf")
|
|
else:
|
|
return cls.from_str(f"{arch}-unknown-linux-gnu")
|
|
|
|
@classmethod
|
|
def host_default(cls):
|
|
"Returns the default build triple of the host."
|
|
rustc_info = subprocess.check_output(["rustc", "-vV"], text=True)
|
|
match = re.search(r"host: (\S+)", rustc_info)
|
|
if not match:
|
|
raise Exception(f"Cannot parse rustc info: {rustc_info}")
|
|
return cls.from_str(match.group(1))
|
|
|
|
def __str__(self):
|
|
return f"{self.arch}-{self.vendor}-{self.sys}-{self.abi}"
|
|
|
|
|
|
def guess_emulator(native_triple: Triple, build_triple: Triple) -> Optional[List[str]]:
|
|
"Guesses which emulator binary to use to run build_triple on a native_triple machine."
|
|
if build_triple == native_triple:
|
|
return None
|
|
# aarch64 can natively run armv7 code in compatibility mode.
|
|
if build_triple.arch == "armv7" and native_triple.arch == "aarch64":
|
|
return None
|
|
# Use wine64 to run windows binaries on linux
|
|
if build_triple.sys == "windows" and str(native_triple) == "x86_64-unknown-linux-gnu":
|
|
return ["wine64"]
|
|
# Use qemu to run aarch64 on x86
|
|
if build_triple.arch == "aarch64" and native_triple.arch == "x86_64":
|
|
return ["qemu-aarch64-static"]
|
|
# Use qemu to run armv7 on x86
|
|
if build_triple.arch == "armv7" and native_triple.arch == "x86_64":
|
|
return ["qemu-arm-static"]
|
|
raise Exception(f"Don't know how to emulate {build_triple} on {native_triple}")
|
|
|
|
|
|
class TestTarget(object):
|
|
"""
|
|
A test target can be the local host, a VM or a remote devica via SSH.
|
|
|
|
Allows an emulation command to be specified which can run a different build target than the
|
|
devices native triple.
|
|
"""
|
|
|
|
target_str: str
|
|
is_host: bool = True
|
|
vm: Optional[testvm.Arch] = None
|
|
ssh: Optional[Ssh] = None
|
|
|
|
override_build_triple: Optional[Triple] = None
|
|
emulator_cmd: Optional[List[str]] = None
|
|
|
|
@classmethod
|
|
def default(cls):
|
|
return cls(os.environ.get("CROSVM_TEST_TARGET", "host"))
|
|
|
|
def __init__(
|
|
self,
|
|
target_str: str,
|
|
override_build_triple: Optional[Triple] = None,
|
|
emulator_cmd: Optional[List[str]] = None,
|
|
):
|
|
"""target_str can be "vm:arch", "ssh:hostname" or just "host" """
|
|
self.target_str = target_str
|
|
parts = target_str.split(":")
|
|
if len(parts) == 2 and parts[0] == "vm":
|
|
vm_arch = cast(testvm.Arch, parts[1])
|
|
self.vm = vm_arch
|
|
self.ssh = Ssh("localhost", testvm.ssh_cmd_args(vm_arch))
|
|
self.is_host = False
|
|
elif len(parts) == 2 and parts[0] == "ssh":
|
|
self.ssh = Ssh(parts[1])
|
|
self.is_host = False
|
|
elif len(parts) == 1 and parts[0] == "host":
|
|
pass
|
|
else:
|
|
raise Exception(f"Invalid target {target_str}")
|
|
self.override_build_triple = override_build_triple
|
|
|
|
if emulator_cmd is not None:
|
|
self.emulator_cmd = emulator_cmd
|
|
elif override_build_triple:
|
|
self.emulator_cmd = guess_emulator(self.native_triple, override_build_triple)
|
|
|
|
@property
|
|
def is_native(self):
|
|
if not self.override_build_triple:
|
|
return True
|
|
return self.build_triple.arch == self.native_triple.arch
|
|
|
|
@property
|
|
def build_triple(self):
|
|
"""
|
|
Triple to build for to run on this test target.
|
|
|
|
May not be the same as the native_triple of the device if an emulator is used or the triple
|
|
has been overridden.
|
|
"""
|
|
if self.override_build_triple:
|
|
return self.override_build_triple
|
|
return self.native_triple
|
|
|
|
@functools.cached_property
|
|
def native_triple(self):
|
|
"""Native triple of the the device on which the test is running."""
|
|
if self.vm:
|
|
return Triple.from_linux_arch(self.vm)
|
|
elif self.ssh:
|
|
return Triple.from_linux_arch(self.ssh.check_output("arch").strip())
|
|
elif self.is_host:
|
|
return Triple.host_default()
|
|
else:
|
|
raise Exception(f"Invalid TestTarget({self})")
|
|
|
|
def __str__(self):
|
|
if self.emulator_cmd:
|
|
return f"{self.target_str} ({self.build_triple} via {' '.join(self.emulator_cmd)})"
|
|
else:
|
|
return f"{self.target_str} ({self.build_triple})"
|
|
|
|
|
|
def find_rust_lib_dir():
|
|
cargo_path = Path(subprocess.check_output(["rustup", "which", "cargo"], text=True))
|
|
if os.name == "posix":
|
|
return cargo_path.parent.parent.joinpath("lib")
|
|
elif os.name == "nt":
|
|
return cargo_path.parent
|
|
else:
|
|
raise Exception(f"Unsupported build target: {os.name}")
|
|
|
|
|
|
def find_rust_libs():
|
|
lib_dir = find_rust_lib_dir()
|
|
yield from lib_dir.glob("libstd-*")
|
|
yield from lib_dir.glob("libtest-*")
|
|
|
|
|
|
def prepare_remote(ssh: Ssh, extra_files: List[Path] = []):
|
|
print("Preparing remote")
|
|
ssh.upload_files(list(find_rust_libs()) + extra_files)
|
|
pass
|
|
|
|
|
|
def prepare_target(target: TestTarget, extra_files: List[Path] = []):
|
|
if target.vm:
|
|
testvm.build_if_needed(target.vm)
|
|
testvm.wait(target.vm)
|
|
if target.ssh:
|
|
prepare_remote(target.ssh, extra_files)
|
|
|
|
|
|
def get_cargo_env(target: TestTarget):
|
|
"""Environment variables to make cargo use the test target."""
|
|
env: Dict[str, str] = BUILD_ENV.copy()
|
|
cargo_target = str(target.build_triple)
|
|
upper_target = cargo_target.upper().replace("-", "_")
|
|
env["CARGO_BUILD_TARGET"] = cargo_target
|
|
if not target.is_host:
|
|
env[f"CARGO_TARGET_{upper_target}_RUNNER"] = f"{SCRIPT_PATH} exec-file"
|
|
env["CROSVM_TEST_TARGET"] = target.target_str
|
|
return env
|
|
|
|
|
|
def write_envrc(values: Dict[str, str]):
|
|
with open(ENVRC_PATH, "w") as file:
|
|
for key, value in values.items():
|
|
file.write(f'export {key}="{value}"\n')
|
|
|
|
|
|
def set_target(target: TestTarget):
|
|
prepare_target(target)
|
|
write_envrc(get_cargo_env(target))
|
|
print(f"Test target: {target}")
|
|
print(f"Target Architecture: {target.build_triple}")
|
|
|
|
|
|
def exec_file_on_target(
|
|
target: TestTarget,
|
|
filepath: Path,
|
|
timeout: int,
|
|
args: List[str] = [],
|
|
extra_files: List[Path] = [],
|
|
**kwargs: Any,
|
|
):
|
|
"""Executes a file on the test target.
|
|
|
|
The file is uploaded to the target's home directory (if it's an ssh or vm
|
|
target) plus any additional extra files provided, then executed and
|
|
deleted afterwards.
|
|
|
|
If the test target is 'host', files will just be executed locally.
|
|
|
|
Timeouts will trigger a subprocess.TimeoutExpired exception, which contanins
|
|
any output produced by the subprocess until the timeout.
|
|
"""
|
|
env = os.environ.copy()
|
|
prefix = target.emulator_cmd if target.emulator_cmd else []
|
|
if not target.ssh:
|
|
# Allow test binaries to find rust's test libs.
|
|
if os.name == "posix":
|
|
env["LD_LIBRARY_PATH"] = str(find_rust_lib_dir())
|
|
elif os.name == "nt":
|
|
if not env["PATH"]:
|
|
env["PATH"] = str(find_rust_lib_dir())
|
|
else:
|
|
env["PATH"] += ";" + str(find_rust_lib_dir())
|
|
else:
|
|
raise Exception(f"Unsupported build target: {os.name}")
|
|
|
|
cmd_line = [*prefix, str(filepath), *args]
|
|
return subprocess.run(
|
|
cmd_line,
|
|
env=env,
|
|
timeout=timeout,
|
|
text=True,
|
|
**kwargs,
|
|
)
|
|
else:
|
|
filename = Path(filepath).name
|
|
target.ssh.upload_files([filepath] + extra_files, quiet=True)
|
|
cmd_line = [*prefix, f"./{filename}", *args]
|
|
try:
|
|
result = target.ssh.run(
|
|
f"chmod +x {filename} && sudo LD_LIBRARY_PATH=. {' '.join(cmd_line)}",
|
|
timeout=timeout,
|
|
text=True,
|
|
**kwargs,
|
|
)
|
|
finally:
|
|
# Remove uploaded files regardless of test result
|
|
all_filenames = [filename] + [f.name for f in extra_files]
|
|
target.ssh.check_output(f"sudo rm {' '.join(all_filenames)}")
|
|
return result
|
|
|
|
|
|
def exec_file(
|
|
target: TestTarget,
|
|
filepath: Path,
|
|
args: List[str] = [],
|
|
timeout: int = 60,
|
|
extra_files: List[Path] = [],
|
|
):
|
|
if not filepath.exists():
|
|
raise Exception(f"File does not exist: {filepath}")
|
|
|
|
print(f"Executing `{Path(filepath).name} {' '.join(args)}` on {target}")
|
|
try:
|
|
sys.exit(exec_file_on_target(target, filepath, timeout, args, extra_files).returncode)
|
|
except subprocess.TimeoutExpired as e:
|
|
print(f"Process timed out after {e.timeout}s")
|
|
|
|
|
|
def main():
|
|
COMMANDS = [
|
|
"set",
|
|
"exec-file",
|
|
]
|
|
|
|
parser = argparse.ArgumentParser(usage=USAGE)
|
|
parser.add_argument("command", choices=COMMANDS)
|
|
parser.add_argument("--target", type=str, help="Override default test target.")
|
|
parser.add_argument(
|
|
"--build-target",
|
|
type=str,
|
|
help="Override target build triple (e.g. x86_64-unknown-linux-gnu).",
|
|
)
|
|
parser.add_argument("--arch", help="Deprecated. Please use --build-target instead."),
|
|
parser.add_argument(
|
|
"--extra-files",
|
|
type=str,
|
|
nargs="*",
|
|
default=[],
|
|
help="Additional files required by the binary to execute.",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=int,
|
|
default=60,
|
|
help="Kill the process after the specified timeout.",
|
|
)
|
|
parser.add_argument("remainder", nargs=argparse.REMAINDER)
|
|
args = parser.parse_args()
|
|
|
|
if args.arch:
|
|
print("--arch is deprecated. Please use --build-target instead.")
|
|
|
|
if args.command == "set":
|
|
if len(args.remainder) != 1:
|
|
parser.error("Need to specify a target.")
|
|
set_target(TestTarget(args.remainder[0], args.build_target))
|
|
return
|
|
|
|
if args.target:
|
|
target = TestTarget(args.target)
|
|
else:
|
|
target = TestTarget.default()
|
|
|
|
if args.command == "exec-file":
|
|
if len(args.remainder) < 1:
|
|
parser.error("Need to specify a file to execute.")
|
|
exec_file(
|
|
target,
|
|
Path(args.remainder[0]),
|
|
args=args.remainder[1:],
|
|
timeout=args.timeout,
|
|
extra_files=[Path(f) for f in args.extra_files],
|
|
)
|