crates/gunnar-recipe/tests/metered_symbols.rs
//! **The community `gunnar-server` binary contains NO entitlement code; the
//! metered one does — read off the symbol tables, not off a declaration.**
//!
//! The metered variant promises a self-hoster a binary that does not
//! CONTAIN plan enforcement, in the sense `OptionAudit` gives the word: no
//! symbol of the family in the artifact. `gunnar-cli/tests/feature_split.rs`
//! proves it on the resolved graph (`monetize-embed` absent); this proves it
//! on the ARTIFACT, with the same reader `nornir_build_thing::OptionAudit`
//! uses (`linkage::demangled_symbols`), so the two cannot disagree about what
//! a binary links.
//!
//! Both directions, always: the metered build MUST show the families, or the
//! community build's clean table is a table nobody could have found anything
//! in. And the reader must have MEASURED: a stripped binary reports `None`
//! and is a failure here, not a pass — the exact silent green
//! `linkage::demangled_symbols` documents.
//!
//! It builds `gunnar-server` twice (dev profile, host target, the two
//! feature sets of the `gunnar-server-thin` and `gunnar-server-metered-thin`
//! rows minus the appliance-only knobs), which is slow by design: there is no
//! cheaper honest way to read a binary than to have one.
use std::path::PathBuf;
use std::process::Command;
use nornir_build_thing::linkage::{count_families, demangled_symbols};
/// Symbol families that exist only with `metered`. Substring-matched on
/// demangled names, like every family `count_families` takes.
///
/// `gunnar_ssh::plan` is NOT here since the browse-replica work (2026-09-04): it is
/// the `PlanGate` trait, the gate chain and the community `DiskHeadroomGate`
/// (the ruling: "two implementations — 1) free lunch, checking disk before a new
/// repo is created; 2) the monetize-bound one"). The community binary carries
/// it on purpose; the metered LINK of that chain lives in
/// `gunnar_server::entitlement`, which stays in this list.
const ENTITLEMENT_FAMILIES: &[&str] = &[
"gunnar_server::entitlement",
"gunnar_grpc::service::entitlement",
"gunnar_core::entitlement",
"monetize_embed",
];
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("crates/gunnar-build-thing sits two below the root")
.to_path_buf()
}
/// Build the server binary with exactly `features`, and hand back its path.
///
/// The path comes from cargo's own `compiler-artifact` message, never from a
/// guessed `<root>/target`: this laptop's `~/.cargo/config.toml` sets a global
/// `build.target-dir`, and a guard that looked in the wrong directory passed
/// only when `CARGO_TARGET_DIR` happened to be exported (2026-09-04, the
/// entitlement merge — "no binary at …/target/debug/gunnar-server").
fn build_server(features: &str) -> PathBuf {
let out = Command::new(env!("CARGO"))
.current_dir(workspace_root())
.args([
"build",
"-q",
"--message-format=json-render-diagnostics",
"-p",
"gunnar-server",
"--bin",
"gunnar-server",
"--no-default-features",
"--features",
features,
])
.output()
.expect("cargo runs");
// cargo's own words ride along: with `-q` and json-render-diagnostics
// the reason is on stderr, and a bare "failed" (all this said until
// 2026-09-29, when it was red on release-3 and green on release-2 with
// nothing to tell the two apart) sends the reader off to rebuild by hand.
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);
// The last `compiler-artifact` line for the bin carries `"executable":"<path>"`.
let bin = stdout
.lines()
.filter(|l| l.contains(r#""compiler-artifact""#) && l.contains(r#""name":"gunnar-server""#))
.filter_map(|l| {
const KEY: &str = r#""executable":""#;
let i = l.find(KEY)? + KEY.len();
let rest = &l[i..];
let j = rest.find('"')?;
Some(PathBuf::from(&rest[..j]))
})
.next_back()
.unwrap_or_else(|| {
panic!("cargo reported no executable for gunnar-server --features {features}")
});
assert!(bin.is_file(), "no binary at {}", bin.display());
bin
}
/// `(family, symbol count)` pairs.
type Families = Vec<(String, usize)>;
fn families_in(features: &str) -> (Families, Families) {
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("gunnar_server::")),
"the symbol table of {} names no gunnar_server code at all, so a clean read of it \
would be vacuous",
bin.display()
);
let counts = count_families(&symbols, ENTITLEMENT_FAMILIES);
// Printed, so `--nocapture` is the symbol audit's report: which binary,
// how many symbols, which families and how many of each.
println!(
"symbol audit --features {features}: {} symbols; found {:?}; glue-only {:?}",
symbols.len(),
counts.found,
counts.glue_only
);
(counts.found, counts.glue_only)
}
#[test]
fn the_community_binary_has_zero_entitlement_symbols_and_the_metered_one_has_them() {
// Sequential on purpose: both builds land at the same path.
let (community, community_glue) = families_in("server,engine-znippy");
assert!(
community.is_empty() && community_glue.is_empty(),
"the community gunnar-server CONTAINS entitlement code: {community:?} (glue-only: \
{community_glue:?}). Off means the module never enters \
codegen, not that it declines to run"
);
let (metered, _) = families_in("server,engine-znippy,metered");
let present: Vec<&str> = metered.iter().map(|(f, _)| f.as_str()).collect();
for family in [
"gunnar_server::entitlement",
"monetize_embed",
"gunnar_core::entitlement",
] {
assert!(
present.contains(&family),
"the metered gunnar-server has no `{family}` symbols, so the community table's \
emptiness above proves nothing: {metered:?}"
);
}
}