123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- #!/usr/bin/env python3
- # Copyright 2023 The ChromiumOS Authors
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- import argparse
- import json
- from multiprocessing.pool import ThreadPool
- import shlex
- import shutil
- from fnmatch import fnmatch
- from pathlib import Path
- from typing import Any, List, Tuple
- from impl.common import (
- CROSVM_ROOT,
- all_tracked_files,
- chdir,
- cmd,
- cwd_context,
- parallel,
- print_timing_info,
- quoted,
- record_time,
- )
- # List of globs matching files in the source tree required by tests at runtime.
- # This is hard-coded specifically for crosvm tests.
- TEST_DATA_FILES = [
- # Requried by nextest to obtain metadata
- "*.toml",
- # Configured by .cargo/config.toml to execute tests with the right emulator
- ".cargo/runner.py",
- # Requried by plugin tests
- "crosvm_plugin/crosvm.h",
- "tests/plugin.policy",
- ]
- TEST_DATA_EXCLUDE = [
- # config.toml is configured for x86 hosts. We cannot use that for remote tests.
- ".cargo/config.toml",
- ]
- cargo = cmd("cargo")
- tar = cmd("tar")
- rust_strip = cmd("rust-strip")
- def collect_rust_libs():
- "Collect rust shared libraries required by the tests at runtime."
- lib_dir = Path(cmd("rustc --print=sysroot").stdout()) / "lib"
- for lib_file in lib_dir.glob("libstd-*"):
- yield (lib_file, Path("debug/deps") / lib_file.name)
- for lib_file in lib_dir.glob("libtest-*"):
- yield (lib_file, Path("debug/deps") / lib_file.name)
- def collect_test_binaries(metadata: Any, strip: bool):
- "Collect all test binaries that are needed to run the tests."
- target_dir = Path(metadata["rust-build-meta"]["target-directory"])
- test_binaries = [
- Path(suite["binary-path"]).relative_to(target_dir)
- for suite in metadata["rust-binaries"].values()
- ]
- non_test_binaries = [
- Path(binary["path"])
- for crate in metadata["rust-build-meta"]["non-test-binaries"].values()
- for binary in crate
- ]
- def process_binary(binary_path: Path):
- source_path = target_dir / binary_path
- destination_path = binary_path
- if strip:
- stripped_path = source_path.with_suffix(".stripped")
- if (
- not stripped_path.exists()
- or source_path.stat().st_ctime > stripped_path.stat().st_ctime
- ):
- rust_strip(f"--strip-all {source_path} -o {stripped_path}").fg()
- return (stripped_path, destination_path)
- else:
- return (source_path, destination_path)
- # Parallelize rust_strip calls.
- pool = ThreadPool()
- return pool.map(process_binary, test_binaries + non_test_binaries)
- def collect_test_data_files():
- "List additional files from the source tree that are required by tests at runtime."
- for file in all_tracked_files():
- for glob in TEST_DATA_FILES:
- if fnmatch(str(file), glob):
- if str(file) not in TEST_DATA_EXCLUDE:
- yield (file, file)
- break
- def collect_files(metadata: Any, output_directory: Path, strip_binaries: bool):
- # List all files we need as (source path, path in output_directory) tuples
- manifest: List[Tuple[Path, Path]] = [
- *collect_test_binaries(metadata, strip=strip_binaries),
- *collect_rust_libs(),
- *collect_test_data_files(),
- ]
- # Create all target directories
- for folder in set((output_directory / d).parent.resolve() for _, d in manifest):
- folder.mkdir(exist_ok=True, parents=True)
- # Use multiple processes of rsync copy the files fast (and only if needed)
- parallel(
- *(
- cmd("rsync -a", source, output_directory / destination)
- for source, destination in manifest
- )
- ).fg()
- def generate_run_script(metadata: Any, output_directory: Path):
- # Generate metadata files for nextest
- binares_metadata_file = "binaries-metadata.json"
- (output_directory / binares_metadata_file).write_text(json.dumps(metadata))
- cargo_metadata_file = "cargo-metadata.json"
- cargo("metadata --format-version 1").write_to(output_directory / cargo_metadata_file)
- # Put together command line to run nextest
- run_cmd = [
- "cargo-nextest",
- "nextest",
- "run",
- f"--binaries-metadata={binares_metadata_file}",
- f"--cargo-metadata={cargo_metadata_file}",
- "--target-dir-remap=.",
- "--workspace-remap=.",
- ]
- command_line = [
- "#!/usr/bin/env bash",
- 'cd "$(dirname "${BASH_SOURCE[0]}")" || die',
- f'{shlex.join(run_cmd)} "$@"',
- ]
- # Write command to a unix shell script
- shell_script = output_directory / "run.sh"
- shell_script.write_text("\n".join(command_line))
- shell_script.chmod(0o755)
- # TODO(denniskempin): Add an equivalent windows bash script
- def generate_archive(output_directory: Path, output_archive: Path):
- with cwd_context(output_directory.parent):
- tar("-ca", output_directory.name, "-f", output_archive).fg()
- def main():
- """
- Builds a package to execute tests remotely.
- ## Basic usage
- ```
- $ tools/nextest_package -o foo.tar.zst ... nextest args
- ```
- The archive will contain all necessary test binaries, required shared libraries and test data
- files required at runtime.
- A cargo nextest binary is included along with a `run.sh` script to invoke it with the required
- arguments. THe archive can be copied anywhere and executed:
- ```
- $ tar xaf foo.tar.zst && cd foo.tar.d && ./run.sh
- ```
- ## Nextest Arguments
- All additional arguments will be passed to `nextest list`. Additional arguments to `nextest run`
- can be passed to the `run.sh` invocation.
- For example:
- ```
- $ tools/nextest_package -d foo --tests
- $ cd foo && ./run.sh --test-threads=1
- ```
- Will only list and package integration tests (--tests) and run them with --test-threads=1.
- ## Stripping Symbols
- Debug symbols are stripped by default to reduce the package size. This can be disabled via
- the `--no-strip` argument.
- """
- parser = argparse.ArgumentParser()
- parser.add_argument("--no-strip", action="store_true")
- parser.add_argument("--output-directory", "-d")
- parser.add_argument("--output-archive", "-o")
- parser.add_argument("--clean", action="store_true")
- parser.add_argument("--timing-info", action="store_true")
- (args, nextest_list_args) = parser.parse_known_args()
- chdir(CROSVM_ROOT)
- # Determine output archive / directory
- output_directory = Path(args.output_directory).resolve() if args.output_directory else None
- output_archive = Path(args.output_archive).resolve() if args.output_archive else None
- if not output_directory and output_archive:
- output_directory = output_archive.with_suffix(".d")
- if not output_directory:
- print("Must specify either --output-directory or --output-archive")
- return
- if args.clean and output_directory.exists():
- shutil.rmtree(output_directory)
- with record_time("Listing tests"):
- cargo(
- "nextest list",
- *(quoted(a) for a in nextest_list_args),
- ).fg()
- with record_time("Listing tests metadata"):
- metadata = cargo(
- "nextest list --list-type binaries-only --message-format json",
- *(quoted(a) for a in nextest_list_args),
- ).json()
- with record_time("Collecting files"):
- collect_files(metadata, output_directory, strip_binaries=not args.no_strip)
- generate_run_script(metadata, output_directory)
- if output_archive:
- with record_time("Generating archive"):
- generate_archive(output_directory, output_archive)
- if args.timing_info:
- print_timing_info()
- if __name__ == "__main__":
- main()
|