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