From d001612feeed9b19657ce6bb35e973835afdba8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0tetiar?= Date: Wed, 22 Jul 2026 13:39:44 +0200 Subject: [PATCH] ci: cram: resolve comma-separated component unions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-component changes currently require separate CRAM jobs. They repeat the board boot. The automatic selector needs one resolver invocation that can cover all affected component buckets. Accept comma-separated components and build their ordered union from the legacy full-suite order. Prefix sanity tests once, remove duplicate tests and retain the existing behavior for every single component and full. Compare all five legacy selections on all six boards with the CI-7 base. All 30 outputs are byte-identical. Verify forward and reverse two-component unions on all six boards, including stable ordering and uniqueness. Assisted-by: codex:gpt-5.6-sol References: CI-9 Signed-off-by: Petr Štetiar --- .gitlab/tests/cram/README.md | 12 +++++-- .../cram/scripts/cram_component_bucketing.py | 33 +++++++++++++++++++ .../cram/scripts/resolve-component-tests.py | 13 ++++---- 3 files changed, 48 insertions(+), 10 deletions(-) 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