mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
ci: Check that every matrix config reaches the tier it declares
CI runs only the jobs generate.py emits, so a config that falls out of the matrix size it belongs to takes its coverage with it and fails nothing. Add validate_linux_matrices(), called on every invocation, which checks that no two configs expand to the same name, that minimal is part of full and full part of extended, that each config reaches the sizes its flags ask for and no others, and that the extended tier is not empty. The checks name no config, only the flags, so adding or removing one needs no edit there. Reject a config declaring both "minimal" and "extended", which the filtering would drop from the minimal matrix, the opposite of what it asked for. Extract expand_linux_config() so the checks and the matrix builder cannot disagree about what a config expands to. The script never sees the event that triggered the run, so it cannot tell whether this run should have asked for the extended configs at all, and a condition that stops matching the events would drop them and fail nothing. So move the three GENERATE_ variables to job level, where the generate step and a new check read the same values, and have that check re-run the generator with --extended and compare. It runs only on the events that must produce the extended matrix. Proved by mutation: seven edits to linux.json and to the filtering each fail, and the message names the config that moved. The generated matrix is byte-identical to the previous one across all seven argument combinations, so the extraction changed no job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
172
.github/scripts/strategy-matrix/generate.py
vendored
172
.github/scripts/strategy-matrix/generate.py
vendored
@@ -109,6 +109,13 @@ class LinuxConfig:
|
||||
def __post_init__(self) -> None:
|
||||
if isinstance(self.package, dict):
|
||||
self.package = PackageConfig(**self.package)
|
||||
# The two flags pull in opposite directions: 'minimal' asks for the
|
||||
# smallest matrix, 'extended' for the largest. The filtering would drop
|
||||
# such a config from the minimal matrix, which reads as the opposite of
|
||||
# what it declared, so reject the pair instead.
|
||||
assert not (
|
||||
self.minimal and self.extended
|
||||
), "a config cannot be both 'minimal' and 'extended'."
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -221,6 +228,49 @@ _ARCHS: dict[str, Architecture] = {
|
||||
}
|
||||
|
||||
|
||||
def expand_linux_config(
|
||||
distro: str, cfg: LinuxConfig, image_tag: str
|
||||
) -> list[MatrixEntry]:
|
||||
"""Expand one Linux config over the cross-product of its lists.
|
||||
|
||||
Kept apart from the size filtering in expand_linux_matrix so that
|
||||
validate_linux_matrices can ask what a single config expands to without
|
||||
repeating the cross-product.
|
||||
|
||||
@param distro The distro key the config is listed under in linux.json.
|
||||
@param cfg The config to expand.
|
||||
@param image_tag The tag of the nix image the entries build in.
|
||||
@return One entry per (compiler, build type, sanitizer, architecture)
|
||||
combination the config's lists produce.
|
||||
"""
|
||||
# An empty sanitizers list means "one entry with no sanitizer".
|
||||
effective_sanitizers = cfg.sanitizers or [""]
|
||||
effective_archs = {arch: _ARCHS[arch] for arch in cfg.arch}
|
||||
|
||||
return [
|
||||
MatrixEntry(
|
||||
config_name=config_name(
|
||||
distro, compiler, build_type, arch, cfg.suffix, sanitizer
|
||||
),
|
||||
image=f"ghcr.io/xrplf/xrpld/nix-{distro}:{image_tag}",
|
||||
cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args),
|
||||
cmake_target="all",
|
||||
build_only=False,
|
||||
benchmark=cfg.benchmark,
|
||||
build_type=build_type,
|
||||
architecture=arch_info,
|
||||
sanitizers=sanitizer,
|
||||
compiler=compiler,
|
||||
)
|
||||
for compiler, build_type, sanitizer, (arch, arch_info) in itertools.product(
|
||||
cfg.compiler,
|
||||
cfg.build_type,
|
||||
effective_sanitizers,
|
||||
effective_archs.items(),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def expand_linux_matrix(
|
||||
linux: LinuxFile, minimal: bool, extended: bool = False
|
||||
) -> list[MatrixEntry]:
|
||||
@@ -246,37 +296,94 @@ def expand_linux_matrix(
|
||||
continue
|
||||
if not extended and cfg.extended:
|
||||
continue
|
||||
# An empty sanitizers list means "one entry with no sanitizer".
|
||||
effective_sanitizers = cfg.sanitizers or [""]
|
||||
effective_archs = {arch: _ARCHS[arch] for arch in cfg.arch}
|
||||
|
||||
for compiler, build_type, sanitizer, (arch, arch_info) in itertools.product(
|
||||
cfg.compiler,
|
||||
cfg.build_type,
|
||||
effective_sanitizers,
|
||||
effective_archs.items(),
|
||||
):
|
||||
name = config_name(
|
||||
distro, compiler, build_type, arch, cfg.suffix, sanitizer
|
||||
)
|
||||
entries.append(
|
||||
MatrixEntry(
|
||||
config_name=name,
|
||||
image=f"ghcr.io/xrplf/xrpld/nix-{distro}:{linux.image_tag}",
|
||||
cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args),
|
||||
cmake_target="all",
|
||||
build_only=False,
|
||||
benchmark=cfg.benchmark,
|
||||
build_type=build_type,
|
||||
architecture=arch_info,
|
||||
sanitizers=sanitizer,
|
||||
compiler=compiler,
|
||||
)
|
||||
)
|
||||
entries += expand_linux_config(distro, cfg, linux.image_tag)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def validate_linux_matrices(linux: LinuxFile) -> None:
|
||||
"""Check that the three matrix sizes nest, and hold the configs they should.
|
||||
|
||||
CI runs only the jobs this script emits, so a config that falls out of the
|
||||
size it belongs to takes its coverage with it and fails nothing. These checks
|
||||
run on every invocation, so the drop fails matrix generation instead.
|
||||
|
||||
The checks name no config, only the flags, so adding or removing a config
|
||||
needs no edit here.
|
||||
|
||||
@param linux The parsed linux.json.
|
||||
@raise AssertionError If two configs expand to the same name, if the sizes do
|
||||
not nest, or if a config reaches a size it does not belong to.
|
||||
"""
|
||||
minimal_names = {e.config_name for e in expand_linux_matrix(linux, minimal=True)}
|
||||
full_names = {e.config_name for e in expand_linux_matrix(linux, minimal=False)}
|
||||
extended_names = {
|
||||
e.config_name for e in expand_linux_matrix(linux, minimal=False, extended=True)
|
||||
}
|
||||
|
||||
# The names each config expands to, paired with the config, so that the
|
||||
# checks below expand every config once.
|
||||
per_config = [
|
||||
(
|
||||
distro,
|
||||
cfg,
|
||||
{e.config_name for e in expand_linux_config(distro, cfg, linux.image_tag)},
|
||||
)
|
||||
for distro, configs in linux.configs.items()
|
||||
for cfg in configs
|
||||
]
|
||||
|
||||
# A config name is also the name of the artifacts the job uploads, so two
|
||||
# configs that expand to the same name overwrite each other. The per-config
|
||||
# checks below also need a name to belong to one config only.
|
||||
all_names = [n for _, _, names in per_config for n in names]
|
||||
duplicates = sorted({n for n in all_names if all_names.count(n) > 1})
|
||||
assert not duplicates, f"configs expand to duplicate names: {duplicates}."
|
||||
|
||||
# The sizes nest, so a larger one only ever adds. Were 'extended' to replace
|
||||
# the full matrix rather than widen it, the nightly would test less than a
|
||||
# labeled pull request does, which is the opposite of the intent.
|
||||
assert minimal_names <= full_names, (
|
||||
"the minimal matrix is not part of the full one, missing: "
|
||||
f"{sorted(minimal_names - full_names)}."
|
||||
)
|
||||
assert full_names <= extended_names, (
|
||||
"the full matrix is not part of the extended one, missing: "
|
||||
f"{sorted(full_names - extended_names)}."
|
||||
)
|
||||
|
||||
# Every config reaches the sizes its flags ask for, and no others.
|
||||
for distro, cfg, names in per_config:
|
||||
if cfg.extended:
|
||||
assert names <= extended_names, (
|
||||
f"{distro} config flagged 'extended' is missing from the "
|
||||
f"extended matrix: {sorted(names - extended_names)}."
|
||||
)
|
||||
assert not names & full_names, (
|
||||
f"{distro} config flagged 'extended' also reaches the full "
|
||||
f"matrix: {sorted(names & full_names)}."
|
||||
)
|
||||
else:
|
||||
assert names <= full_names, (
|
||||
f"{distro} config is missing from the full matrix: "
|
||||
f"{sorted(names - full_names)}."
|
||||
)
|
||||
if cfg.minimal:
|
||||
assert names <= minimal_names, (
|
||||
f"{distro} config flagged 'minimal' is missing from the "
|
||||
f"minimal matrix: {sorted(names - minimal_names)}."
|
||||
)
|
||||
|
||||
# The 'extended' tier costs a flag here, a condition in
|
||||
# reusable-strategy-matrix.yml, and the check after it. An empty tier leaves
|
||||
# all three as dead weight that still reads as working, so require a holder.
|
||||
# Checked last, because the checks above name the config that went missing.
|
||||
assert extended_names > full_names, (
|
||||
"no config is flagged 'extended', so the extended matrix is the full one. "
|
||||
"Either flag the config that needs the tier, or remove the tier."
|
||||
)
|
||||
|
||||
|
||||
def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
|
||||
"""Generate the packaging matrix from the configs that carry a 'package' map.
|
||||
|
||||
@@ -410,17 +517,20 @@ if __name__ == "__main__":
|
||||
|
||||
matrix: list[MatrixEntry] | list[PackagingEntry] = []
|
||||
|
||||
# Checked on every invocation, including the ones that emit another platform
|
||||
# or the packaging matrix, so that no call can pass a broken linux.json.
|
||||
linux = LinuxFile.load(THIS_DIR / "linux.json")
|
||||
validate_linux_matrices(linux)
|
||||
|
||||
if args.packaging:
|
||||
matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json"))
|
||||
matrix = expand_linux_packaging(linux)
|
||||
# One list per format, so each install-test job installs the packages its
|
||||
# own format produced.
|
||||
for package_type, names in package_names_by_type(matrix).items():
|
||||
print(f"{package_type}_package_names={json.dumps(names)}")
|
||||
else:
|
||||
if args.config in ("linux", None):
|
||||
matrix += expand_linux_matrix(
|
||||
LinuxFile.load(THIS_DIR / "linux.json"), args.minimal, args.extended
|
||||
)
|
||||
matrix += expand_linux_matrix(linux, args.minimal, args.extended)
|
||||
if args.config in ("macos", None):
|
||||
matrix += expand_platform_matrix(
|
||||
PlatformFile.load(THIS_DIR / "macos.json"), args.minimal
|
||||
|
||||
Reference in New Issue
Block a user