1
0

nextest_package 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 argparse
  6. import json
  7. from multiprocessing.pool import ThreadPool
  8. import shlex
  9. import shutil
  10. from fnmatch import fnmatch
  11. from pathlib import Path
  12. from typing import Any, List, Tuple
  13. from impl.common import (
  14. CROSVM_ROOT,
  15. all_tracked_files,
  16. chdir,
  17. cmd,
  18. cwd_context,
  19. parallel,
  20. print_timing_info,
  21. quoted,
  22. record_time,
  23. )
  24. # List of globs matching files in the source tree required by tests at runtime.
  25. # This is hard-coded specifically for crosvm tests.
  26. TEST_DATA_FILES = [
  27. # Requried by nextest to obtain metadata
  28. "*.toml",
  29. # Configured by .cargo/config.toml to execute tests with the right emulator
  30. ".cargo/runner.py",
  31. # Requried by plugin tests
  32. "crosvm_plugin/crosvm.h",
  33. "tests/plugin.policy",
  34. ]
  35. TEST_DATA_EXCLUDE = [
  36. # config.toml is configured for x86 hosts. We cannot use that for remote tests.
  37. ".cargo/config.toml",
  38. ]
  39. cargo = cmd("cargo")
  40. tar = cmd("tar")
  41. rust_strip = cmd("rust-strip")
  42. def collect_rust_libs():
  43. "Collect rust shared libraries required by the tests at runtime."
  44. lib_dir = Path(cmd("rustc --print=sysroot").stdout()) / "lib"
  45. for lib_file in lib_dir.glob("libstd-*"):
  46. yield (lib_file, Path("debug/deps") / lib_file.name)
  47. for lib_file in lib_dir.glob("libtest-*"):
  48. yield (lib_file, Path("debug/deps") / lib_file.name)
  49. def collect_test_binaries(metadata: Any, strip: bool):
  50. "Collect all test binaries that are needed to run the tests."
  51. target_dir = Path(metadata["rust-build-meta"]["target-directory"])
  52. test_binaries = [
  53. Path(suite["binary-path"]).relative_to(target_dir)
  54. for suite in metadata["rust-binaries"].values()
  55. ]
  56. non_test_binaries = [
  57. Path(binary["path"])
  58. for crate in metadata["rust-build-meta"]["non-test-binaries"].values()
  59. for binary in crate
  60. ]
  61. def process_binary(binary_path: Path):
  62. source_path = target_dir / binary_path
  63. destination_path = binary_path
  64. if strip:
  65. stripped_path = source_path.with_suffix(".stripped")
  66. if (
  67. not stripped_path.exists()
  68. or source_path.stat().st_ctime > stripped_path.stat().st_ctime
  69. ):
  70. rust_strip(f"--strip-all {source_path} -o {stripped_path}").fg()
  71. return (stripped_path, destination_path)
  72. else:
  73. return (source_path, destination_path)
  74. # Parallelize rust_strip calls.
  75. pool = ThreadPool()
  76. return pool.map(process_binary, test_binaries + non_test_binaries)
  77. def collect_test_data_files():
  78. "List additional files from the source tree that are required by tests at runtime."
  79. for file in all_tracked_files():
  80. for glob in TEST_DATA_FILES:
  81. if fnmatch(str(file), glob):
  82. if str(file) not in TEST_DATA_EXCLUDE:
  83. yield (file, file)
  84. break
  85. def collect_files(metadata: Any, output_directory: Path, strip_binaries: bool):
  86. # List all files we need as (source path, path in output_directory) tuples
  87. manifest: List[Tuple[Path, Path]] = [
  88. *collect_test_binaries(metadata, strip=strip_binaries),
  89. *collect_rust_libs(),
  90. *collect_test_data_files(),
  91. ]
  92. # Create all target directories
  93. for folder in set((output_directory / d).parent.resolve() for _, d in manifest):
  94. folder.mkdir(exist_ok=True, parents=True)
  95. # Use multiple processes of rsync copy the files fast (and only if needed)
  96. parallel(
  97. *(
  98. cmd("rsync -a", source, output_directory / destination)
  99. for source, destination in manifest
  100. )
  101. ).fg()
  102. def generate_run_script(metadata: Any, output_directory: Path):
  103. # Generate metadata files for nextest
  104. binares_metadata_file = "binaries-metadata.json"
  105. (output_directory / binares_metadata_file).write_text(json.dumps(metadata))
  106. cargo_metadata_file = "cargo-metadata.json"
  107. cargo("metadata --format-version 1").write_to(output_directory / cargo_metadata_file)
  108. # Put together command line to run nextest
  109. run_cmd = [
  110. "cargo-nextest",
  111. "nextest",
  112. "run",
  113. f"--binaries-metadata={binares_metadata_file}",
  114. f"--cargo-metadata={cargo_metadata_file}",
  115. "--target-dir-remap=.",
  116. "--workspace-remap=.",
  117. ]
  118. command_line = [
  119. "#!/usr/bin/env bash",
  120. 'cd "$(dirname "${BASH_SOURCE[0]}")" || die',
  121. f'{shlex.join(run_cmd)} "$@"',
  122. ]
  123. # Write command to a unix shell script
  124. shell_script = output_directory / "run.sh"
  125. shell_script.write_text("\n".join(command_line))
  126. shell_script.chmod(0o755)
  127. # TODO(denniskempin): Add an equivalent windows bash script
  128. def generate_archive(output_directory: Path, output_archive: Path):
  129. with cwd_context(output_directory.parent):
  130. tar("-ca", output_directory.name, "-f", output_archive).fg()
  131. def main():
  132. """
  133. Builds a package to execute tests remotely.
  134. ## Basic usage
  135. ```
  136. $ tools/nextest_package -o foo.tar.zst ... nextest args
  137. ```
  138. The archive will contain all necessary test binaries, required shared libraries and test data
  139. files required at runtime.
  140. A cargo nextest binary is included along with a `run.sh` script to invoke it with the required
  141. arguments. THe archive can be copied anywhere and executed:
  142. ```
  143. $ tar xaf foo.tar.zst && cd foo.tar.d && ./run.sh
  144. ```
  145. ## Nextest Arguments
  146. All additional arguments will be passed to `nextest list`. Additional arguments to `nextest run`
  147. can be passed to the `run.sh` invocation.
  148. For example:
  149. ```
  150. $ tools/nextest_package -d foo --tests
  151. $ cd foo && ./run.sh --test-threads=1
  152. ```
  153. Will only list and package integration tests (--tests) and run them with --test-threads=1.
  154. ## Stripping Symbols
  155. Debug symbols are stripped by default to reduce the package size. This can be disabled via
  156. the `--no-strip` argument.
  157. """
  158. parser = argparse.ArgumentParser()
  159. parser.add_argument("--no-strip", action="store_true")
  160. parser.add_argument("--output-directory", "-d")
  161. parser.add_argument("--output-archive", "-o")
  162. parser.add_argument("--clean", action="store_true")
  163. parser.add_argument("--timing-info", action="store_true")
  164. (args, nextest_list_args) = parser.parse_known_args()
  165. chdir(CROSVM_ROOT)
  166. # Determine output archive / directory
  167. output_directory = Path(args.output_directory).resolve() if args.output_directory else None
  168. output_archive = Path(args.output_archive).resolve() if args.output_archive else None
  169. if not output_directory and output_archive:
  170. output_directory = output_archive.with_suffix(".d")
  171. if not output_directory:
  172. print("Must specify either --output-directory or --output-archive")
  173. return
  174. if args.clean and output_directory.exists():
  175. shutil.rmtree(output_directory)
  176. with record_time("Listing tests"):
  177. cargo(
  178. "nextest list",
  179. *(quoted(a) for a in nextest_list_args),
  180. ).fg()
  181. with record_time("Listing tests metadata"):
  182. metadata = cargo(
  183. "nextest list --list-type binaries-only --message-format json",
  184. *(quoted(a) for a in nextest_list_args),
  185. ).json()
  186. with record_time("Collecting files"):
  187. collect_files(metadata, output_directory, strip_binaries=not args.no_strip)
  188. generate_run_script(metadata, output_directory)
  189. if output_archive:
  190. with record_time("Generating archive"):
  191. generate_archive(output_directory, output_archive)
  192. if args.timing_info:
  193. print_timing_info()
  194. if __name__ == "__main__":
  195. main()