ci: cram: resolve comma-separated component unions

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 <petr.stetiar@prplfoundation.org>
This commit is contained in:
Petr Štetiar 2026-07-22 13:39:44 +02:00
parent abebe0921f
commit d001612fee
No known key found for this signature in database
GPG Key ID: 58EE120F30CC02D3
3 changed files with 48 additions and 10 deletions

View File

@ -120,9 +120,15 @@ dependency match the job:
``` ```
The command prints the ordered test paths for cram. Valid component values are The command prints the ordered test paths for cram. Valid component values are
`full`, `sanity`, `prplmesh`, `lcm`, and `prplos`. Use `sanity` to inspect the `full`, `sanity`, `prplmesh`, `lcm`, and `prplos`. A comma-separated value such
always-run block. The `sanity` value is for inspection, not pipeline job as `prplmesh,lcm` resolves the ordered union: the sanity block is emitted once,
selection. 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: Run the manifest lint from that environment as well:

View File

@ -361,6 +361,39 @@ def partition_tests(
return buckets 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: def render_test_paths(paths: Sequence[Path]) -> str:
"""Render ordered test paths for cram's command-line arguments.""" """Render ordered test paths for cram's command-line arguments."""

View File

@ -16,6 +16,7 @@ from cram_component_bucketing import (
partition_tests, partition_tests,
render_test_paths, render_test_paths,
resolve_manifest_path, resolve_manifest_path,
select_component_tests,
) )
@ -37,7 +38,7 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--component", "--component",
required=True, required=True,
choices=("full", "sanity", "prplmesh", "lcm", "prplos"), help="component name or comma-separated component union",
) )
return parser.parse_args(argv) return parser.parse_args(argv)
@ -50,12 +51,10 @@ def main(argv: Sequence[str] | None = None) -> int:
expanded = expand_manifest(args.test_root, manifest) expanded = expand_manifest(args.test_root, manifest)
full = collect_board_tests(args.test_root, args.board) full = collect_board_tests(args.test_root, args.board)
buckets = partition_tests(full, args.test_root, expanded) buckets = partition_tests(full, args.test_root, expanded)
if args.component == "full": components = args.component.split(",")
selected = full if any(not component for component in components):
elif args.component == "sanity": raise BucketingError("component list contains an empty value")
selected = buckets["sanity"] selected = select_component_tests(full, buckets, components)
else:
selected = buckets["sanity"] + buckets[args.component]
except BucketingError as error: except BucketingError as error:
print(f"error: {error}", file=sys.stderr) print(f"error: {error}", file=sys.stderr)
return 2 return 2