diff --git a/.gitlab/tests/cram/README.md b/.gitlab/tests/cram/README.md index 02bc37ae22..eb67f7a63e 100644 --- a/.gitlab/tests/cram/README.md +++ b/.gitlab/tests/cram/README.md @@ -120,9 +120,15 @@ dependency match the job: ``` The command prints the ordered test paths for cram. Valid component values are -`full`, `sanity`, `prplmesh`, `lcm`, and `prplos`. Use `sanity` to inspect the -always-run block. The `sanity` value is for inspection, not pipeline job -selection. +`full`, `sanity`, `prplmesh`, `lcm`, and `prplos`. A comma-separated value such +as `prplmesh,lcm` resolves the ordered union: the sanity block is emitted once, +then each selected bucket in manifest order, so the job still uses one boot. +Repeated components and tests are de-duplicated. Selecting `full`, or selecting +all three split buckets, collapses to the legacy full-suite ordering. Do not put +spaces around the commas. + +Use `sanity` to inspect the always-run block. The `sanity` value is for +inspection, not pipeline job selection. Run the manifest lint from that environment as well: diff --git a/.gitlab/tests/cram/scripts/cram_component_bucketing.py b/.gitlab/tests/cram/scripts/cram_component_bucketing.py index 2d1d67a2e9..208c2678b0 100644 --- a/.gitlab/tests/cram/scripts/cram_component_bucketing.py +++ b/.gitlab/tests/cram/scripts/cram_component_bucketing.py @@ -361,6 +361,39 @@ def partition_tests( return buckets +def select_component_tests( + full: Sequence[Path], + buckets: Mapping[str, List[Path]], + components: Sequence[str], +) -> List[Path]: + """Select an ordered component union with the sanity tests first.""" + + allowed = {"full", "sanity", "prplmesh", "lcm", "prplos"} + unknown = sorted(set(components) - allowed) + if unknown: + raise BucketingError(f"unknown components: {', '.join(unknown)}") + if not components: + raise BucketingError("at least one component is required") + + unique = tuple(dict.fromkeys(components)) + if "full" in unique: + return list(full) + if len(unique) == 1: + component = unique[0] + if component == "sanity": + return list(buckets["sanity"]) + return list(buckets["sanity"] + buckets[component]) + + selected = set(buckets["sanity"]) + for component in unique: + if component != "sanity": + selected.update(buckets[component]) + + sanity = list(buckets["sanity"]) + sanity_set = set(sanity) + return sanity + [path for path in full if path in selected and path not in sanity_set] + + def render_test_paths(paths: Sequence[Path]) -> str: """Render ordered test paths for cram's command-line arguments.""" diff --git a/.gitlab/tests/cram/scripts/resolve-component-tests.py b/.gitlab/tests/cram/scripts/resolve-component-tests.py index 52ec366da2..895e2bff4b 100755 --- a/.gitlab/tests/cram/scripts/resolve-component-tests.py +++ b/.gitlab/tests/cram/scripts/resolve-component-tests.py @@ -16,6 +16,7 @@ from cram_component_bucketing import ( partition_tests, render_test_paths, resolve_manifest_path, + select_component_tests, ) @@ -37,7 +38,7 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser.add_argument( "--component", required=True, - choices=("full", "sanity", "prplmesh", "lcm", "prplos"), + help="component name or comma-separated component union", ) return parser.parse_args(argv) @@ -50,12 +51,10 @@ def main(argv: Sequence[str] | None = None) -> int: expanded = expand_manifest(args.test_root, manifest) full = collect_board_tests(args.test_root, args.board) buckets = partition_tests(full, args.test_root, expanded) - if args.component == "full": - selected = full - elif args.component == "sanity": - selected = buckets["sanity"] - else: - selected = buckets["sanity"] + buckets[args.component] + components = args.component.split(",") + if any(not component for component in components): + raise BucketingError("component list contains an empty value") + selected = select_component_tests(full, buckets, components) except BucketingError as error: print(f"error: {error}", file=sys.stderr) return 2