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

nordisk/gunnar

crates/gunnar-recipe/tests/feature_axis.rs

//! **The feature axis: every feature gunnar declares is built by a variant, or has
//! a written reason saying why nothing compiles it.**
//!
//! ## Why this file exists, and why it is HERE and not in xtask
//!
//! `nornir_build_thing::feature_axis::FeatureAxis::manifest_gate` is the shared
//! verdict on that question, and until now gunnar was one of the repos with a
//! full declaration and no caller: `.nornir/build-variants.toml` carries ten
//! `[build] offers`, six `[build.allow_unbuilt]` excuses and four
//! `[build.allow_unbuilt_bins]` excuses — and **nothing in the repository read
//! any of it**. `znippy-zoomies/xtask/tests/build_variants.rs` was the only
//! caller of the gate anywhere outside edda. A declaration nobody reads is
//! decoration: the six excuses could name features that no longer exist, and a
//! new feature added tomorrow would be built by nothing and say nothing.
//!
//! The obvious home would be gunnar's `xtask`, which is where zoomies put it.
//! It cannot go there: `xtask/Cargo.toml` has **zero dependencies, deliberately**,
//! and the reason is written into the manifest — a workspace member takes part in
//! resolver-2 feature unification for `cargo build --workspace`, so a dependency
//! added to the guard could turn a feature on in a crate the appliance links, and
//! "nothing to unify" is the strongest form of that guarantee. Adding
//! `nornir-build-thing` there to check the feature axis would be the guard
//! becoming the thing it guards against.
//!
//! This crate is the right home instead: it is the build thing itself, it
//! already depends on `nornir-build-thing`, and it OWNS the declaration — so
//! the gate can read `GunnarBuildThing.variants()`, the authority, rather than
//! `.nornir/build-variants.toml`, which is that declaration's inert projection.
//! The declaration and the verdict on the declaration live together.
//!
//! ## What each test is for
//!
//! [`every_declared_feature_is_built_or_written_down`] is the gate. It compiles
//! nothing — a manifest read and a few set differences — so it is always on, which
//! matters: a gate slow enough to skip gets skipped.
//!
//! [`the_feature_gate_can_report_red`] is the measured half. A gate over set
//! differences goes green for two very different reasons, and only one of them is
//! good: the sets agree, or one of the sets is empty. Every way this could be an
//! empty oracle is asserted against by name.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use gunnar_recipe::GunnarBuildThing;
use nornir_build_thing::feature_axis::{manifest_features, FeatureAxis};
use nornir_build_thing::plugin::BuildThing;
use nornir_build_thing::variant::BuildVariantSet;

/// `crates/gunnar-recipe/` → the gunnar repo root. Derived, never spelled, so a
/// moved crate fails loudly instead of scanning an empty tree.
fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(2)
        .expect("this crate sits two levels below the repo root")
        .to_path_buf()
}

/// The root workspace's features plus the DETACHED workspaces' (since 2026-10-22
/// `crates/gunnar-front` and `crates/gunnar-monetize-guest` are their own
/// workspaces, so a lone clone can load the root one): the Build Thing still
/// builds their rows, so their features are still this repo's offers.
fn all_manifest_features() -> BTreeSet<String> {
    let root = repo_root();
    let mut out = manifest_features(&root);
    for detached in ["crates/gunnar-front", "crates/gunnar-monetize-guest"] {
        out.extend(manifest_features(&root.join(detached)));
    }
    out
}

fn axis(set: &BuildVariantSet, manifest: &BTreeSet<String>) -> FeatureAxis {
    FeatureAxis::with_manifest_features("gunnar", set, Some(manifest))
}

/// **The declaration, from the CODE and not from the toml.**
///
/// `.nornir/build-variants.toml` is the recipe's projection — emitted by
/// `cargo run -p gunnar-build-thing -- emit`, asserted byte-for-byte by
/// `tests/toml_parity.rs`, and never read back as authority (that file's
/// `an_edited_toml_has_zero_effect_on_what_the_recipe_declares` measures it).
/// Reading the file here would make this gate a verdict on a projection: it
/// would go green on a stale file whose code had already moved, and the whole
/// point of the gate is to be a verdict on what gunnar declares.
///
/// It is also why the reasons for these excuses are Rust comments in
/// `recipe.rs` rather than toml comments — a comment typed into the emitted
/// file fails parity by construction.
fn declared() -> BuildVariantSet {
    GunnarBuildThing.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 = all_manifest_features();
    if let Err(rot) = axis(&set, &manifest).manifest_gate() {
        panic!(
            "gunnar'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 — that is the right answer far more \
             often than it looks; (2) declare a `[[build.variant]]` that enables it, \
             if a delivered artifact should contain it; (3) write a line in \
             `[build.allow_unbuilt]` saying IN WORDS why nothing compiles it. 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 = all_manifest_features();

    // (a) The oracle is real, and it is gunnar's own table. An empty or partial
    // scan makes the gate above trivially green, and a scan that misses member
    // crates is exactly how that happens (`manifest_features` expands one level
    // of member glob).
    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 [
        "server",
        "client",
        "replication",
        "metered",
        "engine-znippy",
    ] {
        assert!(
            manifest.contains(expect),
            "the scan missed `{expect}`, which is declared in this workspace: \
             {manifest:?}"
        );
    }

    let a = axis(&set, &manifest);

    // (b) Unlike znippy-zoomies, gunnar CONSTRAINS its space, so the `offers`
    // half of the axis has real input here too — and the gate is worth having
    // for both halves rather than only the manifest one.
    assert!(
        !a.offers.is_empty(),
        "gunnar declares `[build] offers`; an empty one would silence the \
         `unoffered` and `unbuilt` halves of the axis by construction"
    );
    assert!(
        !a.built.is_empty(),
        "no declared variant enables ANY feature — then `unbuilt` is every offer \
         and this gate could never have been green"
    );
    // The scan must have found every offer, or the Cargo column is a floor rather
    // than a measurement — which `manifest_gate` refuses, and which is asserted
    // here so the reason is named before the gate names it.
    assert_eq!(
        a.offers_in_manifest,
        a.offers.len(),
        "the scan saw only {} of the {} features gunnar `offers` — the missing \
         ones are declared somewhere this scan does not reach: 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. This is what catches an excuse
    // for a feature that is built now, or that never existed.
    let excused: Vec<String> = a.allow_unbuilt.keys().cloned().collect();
    assert!(
        !excused.is_empty(),
        "`[build.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: either the feature IS built by a \
                 variant (delete the excuse) or the gate cannot see it"
            ));
        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, so the message cannot be acted on: {msg}"
        );
    }
}