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

nordisk/edda

crates/nornir-build-thing/tests/installer_options.rs

//! **The opt-out axis, end to end over a REAL recipe file.**
//!
//! `[[build.installer.options]]` is the declaration that lets an appliance
//! customer say "no WireGuard" / "no DHCP" and get a binary that does not
//! CONTAIN the code — not one that declines to run it. These tests drive the
//! whole chain the way a door does: parse a real `.nornir/build-variants.toml`,
//! resolve toggles, apply values, render the cmdline selector, and push the
//! resolution into the build space so the cargo argv for the packed app binary
//! actually loses the feature.
//!
//! The unit tests in `src/options.rs` hold the grammar. What is held HERE is
//! the wiring nothing else can see: a field that parses but is dropped on the
//! way to the rendered ISO, a content id that does not move when the code
//! inside the medium does, and a `--features` list that keeps a feature the
//! operator switched off. Each of those reads as working.

use std::collections::BTreeMap;

use nornir_build_thing::options::{OptionAudit, parse_cli_options, resolve_options};
use nornir_build_thing::params::NoFixtures;
use nornir_build_thing::variant::BuildVariantSet;

/// A recipe in the shape gunnar's really is: a musl app variant that names its
/// whole diet (`no_default_features = true`), an installer that packs it, two
/// declared opt-outs and the parameters each one brings with it.
const RECIPE: &str = r#"
[[build.variant]]
name = "gunnar-server"
role = "server"
default = true
suites = ["ORIGINAL"]
package = "gunnar-server"
bin = "gunnar-server"
kind = "exe"
no_default_features = true
features = ["server", "engine-znippy", "dhcp-client", "wg-client"]
profile = "release"
target = "x86_64-unknown-linux-musl"

[[build.variant]]
name = "gunnar-installer"
role = "server"
default = false
suites = ["ORIGINAL"]
kind = "installer"

[[build.installer]]
name = "gunnar-installer"
variant = "gunnar-server"
volume_id = "GUNNAR_INSTALL"
boot_params = ["console=ttyS0", "korp.entry=gunnar", "korp.znippy=/app.znippy"]

[[build.installer.params]]
name = "static_ip"
label = "IP-adress / prefix"
kind = "text"
required = true
boot_param = "korp.env.GUNNAR_IP"

[[build.installer.params]]
name = "gateway"
label = "Gateway"
kind = "text"
required = true
boot_param = "korp.env.GUNNAR_GW"

[[build.installer.params]]
name = "dns"
label = "DNS"
kind = "text"
required = true
boot_param = "korp.env.GUNNAR_DNS"

[[build.installer.params]]
name = "hub_conf"
label = "Hub declaration"
kind = "text"
required = false
staged_file = "hub.conf"

[build.installer.fixtures]
hub_conf = "wg-hub"

[[build.installer.options]]
name = "dhcp"
label = "DHCP (eth0)"
default = true
feature = "dhcp-client"
params_when_off = ["static_ip", "gateway", "dns"]
param_kinds = { static_ip = "ipv4-cidr", gateway = "ipv4", dns = "ipv4" }
forbid_symbols = ["gunnar_server::dhcp"]
bakes = { boot_param = "korp.env.GUNNAR_NET", when_on = "eth0:dhcp", when_off = "eth0:static:{static_ip}:{gateway}:{dns}" }

[[build.installer.options]]
name = "wireguard"
label = "WireGuard-klient"
default = true
feature = "wg-client"
params_when_on = ["hub_conf"]
forbid_symbols = ["wg_appliance_core", "x25519", "boringtun"]
"#;

fn load() -> BuildVariantSet {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("build-variants.toml");
    std::fs::write(&path, RECIPE).expect("write recipe");
    let set = BuildVariantSet::load_file(&path).expect("the recipe loads");
    // Keep the dir alive until after the read.
    drop(dir);
    set
}

fn cell(set: &BuildVariantSet) -> nornir_build_thing::InstallerBuild {
    set.installers.iter().find(|i| i.name == "gunnar-installer").expect("the cell").clone()
}

/// LAW 2 on this whole file: if the loader silently dropped
/// `[[build.installer.options]]` — which is exactly what an older reader does
/// with it, benignly — every assertion below would be comparing nothing to
/// nothing and would pass. This is the one that goes red first.
#[test]
fn the_recipe_really_declares_two_options_and_the_loader_really_reads_them() {
    let set = load();
    let c = cell(&set);
    assert_eq!(
        c.options.iter().map(|o| o.name.as_str()).collect::<Vec<_>>(),
        vec!["dhcp", "wireguard"],
        "the loader did not read [[build.installer.options]] at all"
    );
    assert_eq!(c.options[0].feature, "dhcp-client");
    assert!(c.options[0].default, "an opt-out defaults ON");
    assert_eq!(
        c.options[0].param_kinds.get("gateway"),
        Some(&nornir_build_thing::params::ParamKind::Ipv4),
        "the sharper shape rides inside the options table, where an old reader never looks"
    );
    c.validate().expect("the declared roster validates");
}

/// The field must survive the copy into the mastered ISO. `installer_iso()`
/// builds an `IsoBuild` from a fixed field list and then `..Default::default()`,
/// so a forgotten line makes the declaration parse, validate, and vanish.
#[test]
fn the_options_and_their_resolved_states_travel_onto_the_mastered_iso() {
    let set = load();
    let mut c = cell(&set);
    let toggles = parse_cli_options(&["wireguard=off"]).expect("toggles");
    c.apply_cli_selection_with(&toggles, &BTreeMap::new(), &NoFixtures).expect("apply");
    let iso = c.installer_iso();
    assert_eq!(iso.options.len(), 2, "the DECLARATION rode onto the ISO");
    assert_eq!(
        iso.applied_options.iter().map(|o| (o.name.as_str(), o.on)).collect::<Vec<_>>(),
        vec![("dhcp", true), ("wireguard", false)],
        "the RESOLVED STATES rode onto the ISO"
    );
    iso.validate().expect("the mastered ISO validates");
}

/// Two media that differ only by a toggle carry different CODE, so they may
/// never be the same artifact by id.
#[test]
fn a_toggle_moves_the_content_id() {
    let set = load();
    let mut on = cell(&set);
    on.apply_cli_selection_with(&BTreeMap::new(), &BTreeMap::new(), &NoFixtures).expect("on");
    let mut off = cell(&set);
    let toggles = parse_cli_options(&["wireguard=off"]).expect("toggles");
    off.apply_cli_selection_with(&toggles, &BTreeMap::new(), &NoFixtures).expect("off");
    assert_ne!(
        on.content_id(),
        off.content_id(),
        "a medium without the WireGuard client shares an id with one that has it"
    );
    // …and a cell that resolved no options at all keeps the id it always had.
    let untouched = cell(&set);
    assert_eq!(untouched.content_id(), cell(&set).content_id());
}

/// DHCP off: the static address becomes required, is validated as IPv4/CIDR by
/// the sharpened kind, and the NET selector is baked from the values.
#[test]
fn dhcp_off_requires_a_valid_static_address_and_bakes_the_static_selector() {
    let set = load();
    let toggles = parse_cli_options(&["dhcp=off"]).expect("toggles");

    // Nothing supplied ⇒ the refusal names the parameter.
    let mut c = cell(&set);
    let e = format!(
        "{:#}",
        c.apply_cli_selection_with(&toggles, &BTreeMap::new(), &NoFixtures).unwrap_err()
    );
    assert!(e.contains("required parameter 'static_ip'"), "{e}");

    // A value of the wrong shape ⇒ refused by the option's sharpened kind,
    // although the parameter ROW says only `kind = "text"`.
    let mut c = cell(&set);
    let supplied: BTreeMap<String, String> = [
        ("static_ip", "192.168.1.50/24"),
        ("gateway", "not-an-ip"),
        ("dns", "9.9.9.9"),
    ]
    .into_iter()
    .map(|(k, v)| (k.to_string(), v.to_string()))
    .collect();
    let e = format!("{:#}", c.apply_cli_selection_with(&toggles, &supplied, &NoFixtures).unwrap_err());
    assert!(e.contains("is not an IPv4 address"), "{e}");

    // The good arm.
    let mut c = cell(&set);
    let supplied: BTreeMap<String, String> = [
        ("static_ip", "192.168.1.50/24"),
        ("gateway", "192.168.1.1"),
        ("dns", "9.9.9.9"),
    ]
    .into_iter()
    .map(|(k, v)| (k.to_string(), v.to_string()))
    .collect();
    c.apply_cli_selection_with(&toggles, &supplied, &NoFixtures).expect("the static arm applies");
    let params = c.effective_boot_params();
    assert!(
        params.iter().any(|p| p
            == "korp.env.GUNNAR_NET=eth0:static:192.168.1.50/24:192.168.1.1:9.9.9.9"),
        "the static NET selector was not baked: {params:?}"
    );
    assert!(
        !params.iter().any(|p| p == "korp.env.GUNNAR_NET=eth0:dhcp"),
        "the dhcp selector rode along beside the static one: {params:?}"
    );
}

/// DHCP on (nobody said otherwise): the dhcp selector is baked, and supplying
/// a static address is REFUSED rather than silently ignored.
#[test]
fn dhcp_on_bakes_the_dhcp_selector_and_refuses_a_static_address() {
    let set = load();
    let mut c = cell(&set);
    c.apply_cli_selection_with(&BTreeMap::new(), &BTreeMap::new(), &NoFixtures).expect("defaults");
    assert!(c.effective_boot_params().iter().any(|p| p == "korp.env.GUNNAR_NET=eth0:dhcp"));

    let mut c = cell(&set);
    let supplied = [("static_ip".to_string(), "192.168.1.50/24".to_string())].into_iter().collect();
    let e = format!(
        "{:#}",
        c.apply_cli_selection_with(&BTreeMap::new(), &supplied, &NoFixtures).unwrap_err()
    );
    assert!(e.contains("static_ip"), "{e}");
    assert!(e.contains("option 'dhcp' is on"), "{e}");
}

/// WireGuard off: the hub declaration is refused BY NAME. This is the leg the
/// design calls out — a `hub.conf` staged onto a medium whose binary carries no
/// WireGuard client is a lie two files deep.
#[test]
fn wireguard_off_refuses_the_hub_declaration_and_stages_nothing() {
    let set = load();
    let toggles = parse_cli_options(&["wireguard=off"]).expect("toggles");
    let mut c = cell(&set);
    let supplied = [("hub_conf".to_string(), "[Peer]\n".to_string())].into_iter().collect();
    let e = format!("{:#}", c.apply_cli_selection_with(&toggles, &supplied, &NoFixtures).unwrap_err());
    assert!(e.contains("hub_conf"), "{e}");
    assert!(e.contains("option 'wireguard' is off"), "{e}");

    // …and with nothing supplied the medium simply carries no hub.conf.
    let mut c = cell(&set);
    c.apply_cli_selection_with(&toggles, &BTreeMap::new(), &NoFixtures).expect("apply");
    assert!(
        c.installer_iso().staged_param_files().is_empty(),
        "a WireGuard-less medium staged a file for a client that is not in the binary"
    );
}

/// **The step that makes NOT COMPILED IN true rather than promised**: the
/// resolution rewrites the app variant's feature list, so the cargo argv the
/// packed binary is built from loses the feature. Both arms, because a
/// resolution that changed nothing would look identical on the ON side.
#[test]
fn the_resolution_removes_the_feature_from_the_packed_app_binarys_cargo_argv() {
    let set = load();
    let c = cell(&set);

    let baseline = set
        .variants
        .iter()
        .find(|v| v.name == "gunnar-server")
        .expect("the app variant")
        .command_line();
    assert!(baseline.contains("--no-default-features"), "{baseline}");
    assert!(baseline.contains("dhcp-client"), "{baseline}");
    assert!(baseline.contains("wg-client"), "{baseline}");

    let toggles = parse_cli_options(&["wireguard=off", "dhcp=off"]).expect("toggles");
    let resolved = c.resolved_options(&toggles).expect("resolve");
    let mut with_options = set.clone();
    resolved
        .apply_to_set(&c.options, &mut with_options, &c.variant)
        .expect("the app variant takes the resolution");
    let line = with_options
        .variants
        .iter()
        .find(|v| v.name == "gunnar-server")
        .expect("the app variant")
        .command_line();
    assert_eq!(
        line,
        "cargo build -p gunnar-server --bin gunnar-server --features server,engine-znippy \
         --no-default-features --profile release --target x86_64-unknown-linux-musl",
        "the OFF features are still on the cargo command line"
    );
    assert_eq!(resolved.feature_delta(), "features: −dhcp-client −wg-client");

    // The ON arm, through the same path, keeps both.
    let resolved_on = c.resolved_options(&BTreeMap::new()).expect("resolve");
    let mut with_on = set.clone();
    resolved_on.apply_to_set(&c.options, &mut with_on, &c.variant).expect("apply");
    assert_eq!(
        with_on.variants.iter().find(|v| v.name == "gunnar-server").unwrap().command_line(),
        baseline,
        "an all-on resolution must reproduce the declaration byte for byte"
    );
}

/// A recipe whose app variant could smuggle the feature back in through the
/// crate's own `default` list is refused where the resolution is pushed into
/// the build space — BEFORE a binary exists, rather than by an audit it was
/// never able to fail.
#[test]
fn an_app_variant_without_no_default_features_is_refused_at_resolution() {
    let set = load();
    let c = cell(&set);
    let mut broken = set.clone();
    broken
        .variants
        .iter_mut()
        .find(|v| v.name == "gunnar-server")
        .unwrap()
        .no_default_features = false;
    let resolved = c.resolved_options(&BTreeMap::new()).expect("resolve");
    let e = format!(
        "{:#}",
        resolved.apply_to_set(&c.options, &mut broken, &c.variant).unwrap_err()
    );
    assert!(e.contains("no_default_features"), "{e}");
}

/// The symbol read-back, on a REAL object file — this crate's own test binary,
/// which certainly contains `nornir_build_thing` symbols and certainly does not
/// contain a WireGuard client. Both arms, so a guard that always said "clean"
/// cannot hide here.
#[test]
fn the_audit_reads_a_real_binary_and_reports_both_arms() {
    let me = std::env::current_exe().expect("this test binary");

    // The arm that must be RED: forbid a family this binary really has.
    let set = load();
    let mut c = cell(&set);
    c.options[1].forbid_symbols = vec!["nornir_build_thing".to_string()];
    let toggles = parse_cli_options(&["wireguard=off"]).expect("toggles");
    let red = OptionAudit::of(&me, &c.resolved_options(&toggles).expect("resolve"));
    assert!(red.measured, "this test binary must be readable and unstripped");
    assert!(!red.holds(), "the audit failed to see a family that is really here");
    let why = red.refusal().expect("a refusal");
    assert!(why.contains("wireguard is off but 'nornir_build_thing' is present"), "{why}");

    // The arm that must be GREEN: the real declaration, on a binary with no
    // WireGuard client in it.
    let green = OptionAudit::of(&me, &cell(&set).resolved_options(&toggles).expect("resolve"));
    assert!(green.measured);
    assert!(green.holds(), "unexpected violations: {:?}", green.violations);
    // …and the evidence that is NOT linkage is reported rather than dropped.
    // MEASURED 2026-08-22: this test binary carries `p_x25519.c` and
    // `p_x25519_asn1.c` — aws-lc translation units riding in under rustls under
    // `nornir-git`. They are why `is_object_file_symbol` exists: matched as a
    // bare substring, `x25519` would make the guard permanently red on every
    // appliance binary in the constellation, all of which link a TLS stack.
    assert!(
        green.glue_only.iter().any(|(o, f, n)| o == "wireguard" && f == "x25519" && *n >= 2),
        "the C translation units were dropped instead of reclassified: {:?}",
        green.glue_only
    );
}

/// LAW 2 on the reclassification itself: a family whose only evidence is a C
/// FILE symbol must NOT hold when the same family is spelled to reach real
/// code. Proven by naming a family this binary carries as Rust.
#[test]
fn a_c_translation_unit_is_not_linkage_but_a_rust_path_still_is() {
    use nornir_build_thing::linkage::{is_object_file_symbol, count_families};
    assert!(is_object_file_symbol("p_x25519.c"));
    assert!(is_object_file_symbol("sha512.S"));
    assert!(!is_object_file_symbol("x25519_dalek::StaticSecret::new"));
    // A Rust path that merely ENDS in a source extension is still a Rust path.
    assert!(!is_object_file_symbol("wg_appliance_core::keys::from_c"));
    let syms = vec![
        "p_x25519.c".to_string(),
        "x25519_dalek::StaticSecret::new".to_string(),
    ];
    let c = count_families(&syms[..1], &["x25519"]);
    assert!(c.found.is_empty(), "a translation unit counted as linkage: {c:?}");
    assert_eq!(c.glue_only, vec![("x25519".to_string(), 1)]);
    let c = count_families(&syms, &["x25519"]);
    assert_eq!(c.found, vec![("x25519".to_string(), 1)], "real code stopped counting");
}

/// The toggles and the values come off one argv, through one grammar.
#[test]
fn the_door_grammar_parses_both_axes_off_one_argv() {
    let args = [
        "--variant",
        "gunnar-installer",
        "--opt",
        "dhcp=off",
        "--opt=wireguard=off",
        "--param",
        "static_ip=192.168.1.50/24",
    ];
    let parsed = nornir_build_thing::suite_cli::parse(&args).expect("parse");
    assert_eq!(parsed.atom.as_deref(), Some("gunnar-installer"));
    assert_eq!(parsed.opts, vec!["dhcp=off", "wireguard=off"]);
    assert_eq!(parsed.params, vec!["static_ip=192.168.1.50/24"]);
    let toggles = resolve_options(
        &cell(&load()).options,
        &cell(&load()).params,
        &parse_cli_options(&parsed.opts).expect("toggles"),
    )
    .expect("resolve");
    assert_eq!(toggles.features_off, vec!["dhcp-client", "wg-client"]);
}