Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

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}"
        );
    }
}