holger-recipe/tests/dev_narrate_symbols.rs
//! **A RELEASE Holger contains no narrate sender at all — read off the symbol table, and held
//! by the declaration** (owner 2026-10-19: Holger's test media are DEV "EXACTLY like gunnar";
//! gunnar-recipe `tests/dev_narrate_symbols.rs` is the pattern).
//!
//! 1. **The artifact.** holger's server built with a release row's switches has ZERO
//! `nordisk_narrate` symbols; built with `dev-narrate` it has the sender — or the clean table
//! proves nothing. Same reader as the composer's `OptionAudit`.
//! 2. **The declaration.** Only the two `…-narrate` rows name the feature; no crate's `default`
//! list does; every estate medium's `dev-narrate` option defaults OFF and forbids
//! `nordisk_narrate`; ON resolves to the `…-narrate` row and OFF to the payload row.
use std::path::PathBuf;
use std::process::Command;
use holger_recipe::packaging::{DEV_NARRATE_FORBIDDEN, DEV_NARRATE_OPTION};
use holger_recipe::variants::{DEV_NARRATE, NARRATE_SUFFIX};
use nornir_build_thing::linkage::{count_families, demangled_symbols};
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("one below the root")
.to_path_buf()
}
/// Build `holger-mvp` with exactly `features`; its path from cargo's own `compiler-artifact`
/// message (never a guessed target dir).
fn build_server(features: &str) -> PathBuf {
let out = Command::new(env!("CARGO"))
.current_dir(workspace_root())
.env_remove("CARGO_MANIFEST_DIR")
.args([
"build",
"-q",
"--message-format=json-render-diagnostics",
"-p",
"holger",
"--bin",
"holger-mvp",
"--no-default-features",
"--features",
features,
])
.output()
.expect("cargo runs");
assert!(
out.status.success(),
"cargo build --features {features} failed ({}):\n{}",
out.status,
String::from_utf8_lossy(&out.stderr)
.lines()
.rev()
.take(40)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n")
);
let stdout = String::from_utf8_lossy(&out.stdout);
let bin = stdout
.lines()
.filter(|l| {
l.contains(r#""compiler-artifact""#)
&& l.contains(r#""executable":""#)
&& l.contains("holger-mvp")
})
.filter_map(|l| {
const KEY: &str = r#""executable":""#;
let i = l.find(KEY)? + KEY.len();
let rest = &l[i..];
Some(PathBuf::from(&rest[..rest.find('"')?]))
})
.next_back()
.unwrap_or_else(|| {
panic!("cargo reported no executable for holger-mvp --features {features}")
});
assert!(bin.is_file(), "no binary at {}", bin.display());
bin
}
fn narrate_symbols(features: &str) -> Vec<(String, usize)> {
let bin = build_server(features);
let symbols = demangled_symbols(&bin).unwrap_or_else(|| {
panic!(
"{} has no readable symbol table: UNMEASURED is not a pass",
bin.display()
)
});
assert!(
symbols.iter().any(|s| s.contains("holger_server_lib::")),
"{} names no holger code: a clean read would be vacuous",
bin.display()
);
let c = count_families(&symbols, &[DEV_NARRATE_FORBIDDEN]);
println!(
"symbol audit --features {features}: {} symbols; found {:?}; glue-only {:?}",
symbols.len(),
c.found,
c.glue_only
);
let mut all = c.found;
all.extend(c.glue_only);
all
}
#[test]
fn the_release_server_has_zero_narrate_symbols_and_the_dev_one_has_the_sender() {
// A box's switches (the appliance, the pairing loop the sender rides beside), host target.
let release = narrate_symbols("mvp-server,appliance,paired");
assert!(release.is_empty(), "a RELEASE holger CONTAINS narrate code: {release:?}. Off means never compiled in, not declining to send");
let dev = narrate_symbols("mvp-server,appliance,paired,dev-narrate");
assert!(dev.iter().any(|(f, _)| f == DEV_NARRATE_FORBIDDEN), "the DEV holger has no `{DEV_NARRATE_FORBIDDEN}` symbols, so the release table's emptiness proves nothing: {dev:?}");
}
#[test]
fn only_the_narrate_rows_name_the_feature_and_every_estate_medium_defaults_it_off() {
let set = holger_recipe::declared();
let mut rows: Vec<String> = Vec::new();
for v in &set.variants {
if v.features.iter().any(|f| f == DEV_NARRATE) {
assert!(
v.name.ends_with(NARRATE_SUFFIX) && !v.is_default,
"`{}` names `{DEV_NARRATE}` and is not a `…-narrate` dev row",
v.name
);
rows.push(v.name.clone());
} else {
assert!(
!v.name.ends_with(NARRATE_SUFFIX),
"`{}` is called a narrate row and does not build the feature",
v.name
);
}
}
rows.sort();
assert_eq!(
rows,
[
"estate-front-musl-metered-narrate",
"mvp-server-musl-metered-narrate"
]
);
let mut media: Vec<String> = Vec::new();
for i in &set.installers {
for o in i.options.iter().filter(|o| o.name == DEV_NARRATE_OPTION) {
assert!(!o.default, "`{}` turns DEV-NARRATE on by default", i.name);
assert_eq!(o.feature, DEV_NARRATE);
assert!(
o.forbid_symbols.iter().any(|s| s == DEV_NARRATE_FORBIDDEN),
"`{}`'s option does not forbid `{DEV_NARRATE_FORBIDDEN}` when off",
i.name
);
media.push(i.name.clone());
}
}
media.sort();
assert_eq!(
media,
[
"holger-front-installer-metered",
"holger-front-keep",
"holger-installer-metered",
"holger-keep"
],
"the estate's four media carry the option"
);
for krate in ["server/cli/Cargo.toml", "holger-estate-front/Cargo.toml"] {
let text = std::fs::read_to_string(workspace_root().join(krate)).unwrap();
let default = text
.lines()
.find(|l| l.trim_start().starts_with("default ="))
.unwrap_or("");
assert!(
!default.contains("dev-narrate"),
"{krate}: `{default}` puts DEV-NARRATE in every default build"
);
}
}
/// ON and OFF both resolve to a DECLARED build point: ON exactly the payload's `…-narrate`
/// row, OFF exactly the payload row, and the payload keeps `dev-narrate` a switch.
#[test]
fn the_dev_narrate_option_on_and_off_resolves_to_a_declared_row() {
use std::collections::{BTreeMap, BTreeSet};
let set = holger_recipe::declared();
let mut seen = 0;
for i in set
.installers
.iter()
.filter(|i| i.options.iter().any(|o| o.name == DEV_NARRATE_OPTION))
{
let app = set
.variants
.iter()
.find(|v| v.name == i.variant)
.unwrap_or_else(|| panic!("{}: no payload row {}", i.name, i.variant));
assert!(app.no_default_features, "{}: `{}` must build no_default_features, or the crate's default decides behind the option", i.name, app.name);
assert!(
app.open.contains(DEV_NARRATE) && !app.features.iter().any(|f| f == DEV_NARRATE),
"{}: `{}` must keep dev-narrate a switch",
i.name,
app.name
);
let dev = set
.variants
.iter()
.find(|v| v.name == format!("{}{NARRATE_SUFFIX}", app.name))
.unwrap_or_else(|| panic!("{}: no `{}{NARRATE_SUFFIX}` row", i.name, app.name));
for on in [true, false] {
let supplied: BTreeMap<String, bool> =
BTreeMap::from([(DEV_NARRATE_OPTION.to_string(), on)]);
let r = nornir_build_thing::options::resolve_options(&i.options, &i.params, &supplied)
.unwrap_or_else(|e| panic!("{} dev-narrate={on}: {e:#}", i.name));
let got: BTreeSet<String> = r.app_features(app).into_iter().collect();
let want: BTreeSet<String> = if on {
dev.features.iter().cloned().collect()
} else {
app.features.iter().cloned().collect()
};
assert_eq!(
got, want,
"{} dev-narrate={on} resolves to no declared row",
i.name
);
}
seen += 1;
}
assert_eq!(seen, 4);
}