znippy-build-thing/tests/feature_axis.rs
//! **The feature axis: every feature znippy declares is built by a variant, or
//! has a written reason saying why nothing compiles it.**
//!
//! znippy was one of the six repos `stinky` buildred-1 left open: a full
//! `[build] offers` declaration (`iceberg`, `sign`) and no caller of the shared
//! gate anywhere in the repository. gunnar wired it first (2026-09-04) and its
//! first run found six of ten rows wrong; this is the same test, on the same
//! shape — the recipe crate owns the declaration, so the gate reads
//! `ZnippyBuildThing.variants()`, the authority, and not the emitted toml,
//! which is its projection and never read back.
//!
//! [`every_declared_feature_is_built_or_written_down`] compiles nothing — a
//! manifest read and set differences — so it is always on.
//! [`the_feature_gate_can_report_red`] is the LAW-2 half: every way a
//! set-difference gate could be green for the wrong reason, asserted by name.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use nornir_build_thing::feature_axis::{manifest_features, FeatureAxis};
use nornir_build_thing::plugin::BuildThing;
use nornir_build_thing::variant::BuildVariantSet;
use znippy_build_thing::ZnippyBuildThing;
/// `znippy-build-thing/` → the znippy repo root. Derived, so a moved crate fails
/// loudly instead of scanning an empty tree.
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("the recipe crate sits one level below the repo root")
.to_path_buf()
}
fn axis(set: &BuildVariantSet, manifest: &BTreeSet<String>) -> FeatureAxis {
FeatureAxis::with_manifest_features("znippy", set, Some(manifest))
}
fn declared() -> BuildVariantSet {
ZnippyBuildThing.variants()
}
/// **THE GATE.** Every feature any `Cargo.toml` in this workspace declares is
/// enabled by some declared variant, or carries a reason in
/// `[build.allow_unbuilt]` saying why nothing compiles it.
#[test]
fn every_declared_feature_is_built_or_written_down() {
let set = declared();
let manifest = manifest_features(&repo_root());
if let Err(rot) = axis(&set, &manifest).manifest_gate() {
panic!(
"znippy's feature axis is rotting.\n\n{rot}\n\
Fix, in order of preference: (1) DELETE the feature, if it gates no \
`cfg` site and pulls no dependency; (2) declare a `[[build.variant]]` that \
enables it, if a delivered artifact should contain it; (3) write a line in \
`allow_unbuilt()` in `src/recipe.rs` saying IN WORDS why nothing compiles \
it, then re-emit the toml. An empty reason is not a reason, and an excuse \
for a feature that is built now, or that no longer exists, fails here too."
);
}
}
/// **The gate is not vacuous**, in each way it could be.
#[test]
fn the_feature_gate_can_report_red() {
let set = declared();
let manifest = manifest_features(&repo_root());
// (a) The oracle is real, and it is znippy's own table.
assert!(
manifest.len() >= 10,
"the manifest scan found only {} features — it is not seeing the member \
crates, and a gate over an empty oracle is not a pass: {manifest:?}",
manifest.len()
);
for expect in ["iceberg", "sign"] {
assert!(
manifest.contains(expect),
"the scan missed `{expect}`, which znippy offers: {manifest:?}"
);
}
let a = axis(&set, &manifest);
// (b) znippy constrains its space, so the `offers` half has real input.
assert!(!a.offers.is_empty(), "znippy declares `[build] offers`");
assert!(
!a.built.is_empty(),
"no declared variant enables ANY feature — then `unbuilt` is every offer"
);
assert_eq!(
a.offers_in_manifest,
a.offers.len(),
"the scan saw only {} of the {} features znippy `offers`: {:?}",
a.offers_in_manifest,
a.offers.len(),
a.offers
);
// (c) The excuse list is not doing the work alone: drop each excuse in
// turn; the gate must go red and NAME that feature.
let excused: Vec<String> = a.allow_unbuilt.keys().cloned().collect();
assert!(
!excused.is_empty(),
"`allow_unbuilt()` is empty — then (c) is vacuous and this test proves \
nothing about the allowlist"
);
for feat in &excused {
let mut thinner = set.clone();
thinner.allow_unbuilt.remove(feat);
let err = axis(&thinner, &manifest)
.manifest_gate()
.expect_err(&format!(
"with the excuse for `{feat}` removed the gate stayed GREEN, so \
that line excuses nothing"
));
let msg = err.to_string();
assert!(
msg.contains(feat.as_str()),
"the gate went red without the excuse for `{feat}` but did not name it: {msg}"
);
}
}