crates/nornir-build-thing/src/linkage.rs
//! **A `linkage` declaration is a claim about the BINARY — this is where it is
//! checked.**
//!
//! [`variant`](crate::variant) lets a build thing declare `linkage = "thin"` or
//! `"fat"`. Nothing in a TOML file makes that true. Three dwarves variants once
//! declared `embeds = [nornir, holger, skade]` while carrying zero nornir symbols,
//! and holger-ui shipped as the "thin" operator client while linking an in-process
//! Iceberg catalog and a real HTTP gateway. [`LinkageAudit`] reads the produced
//! produced artifact's symbol table (pure-Rust via the `object` parser, demangled
//! like `nm -C`) and says whether the declaration survives contact with it.
//!
//! ## Two design properties, both load-bearing
//!
//! * **An unmeasured audit never passes.** "Could not measure" is its own outcome
//! ([`LinkageVerdict::NotMeasured`]), never folded into "passed". A missing
//! binary, an artifact that will not parse as an object file, an unreadable
//! artifact — each is a FAILURE. Conflating "no evidence of a problem" with
//! "evidence of no problem"
//! is the single most productive bug generator in this constellation.
//! * **A fat variant is never failed for being fat — and never passes for being
//! empty.** A `linkage = "fat"` build is *supposed* to carry the server and the
//! storage engine, so finding them there is the expected result, not a
//! violation. But a guard that passes everything proves nothing: if the fat
//! variant stops embedding, the thin check it is paired against has quietly
//! become vacuous. So a variant that declares it embeds and measurably embeds
//! nothing is [`LinkageVerdict::EmbeddedButEmpty`], which does not hold.
//!
//! ## Why this lives here and not in the forge
//!
//! It used to live in `dwarves::build_thing`, where it could not be reused: dwarves
//! is the whole CI/CD forge (~127 dependencies), so dev-depending on it to reach one
//! struct would make `cargo test -p holger-ui` build the forge. holger therefore
//! re-implemented the contract in its own test file — a twin, and twins have been
//! the richest single source of bugs here. `nornir-build-thing` is the right home:
//! every build thing already depends on it, it is a leaf, and it already owns the
//! `ServiceShape` / `linkage` schema this audits.
//!
//! The symbol read is **pure-Rust**: the `object` parser reads the ELF symbol
//! table in-process and `rustc-demangle` turns each name into the readable path
//! `nm -C` would print — no `nm` subprocess (the constellation LAW: symbol
//! listing goes through `object`/`gimli`, never `nm`). This mirrors
//! `plugin_host`, which already parses object files with `object::File::parse`.
//! `nm -C` demangles, and families such as `nornir::server` only match once the
//! v0/legacy mangling is undone, so demangling is not optional here.
use std::path::{Path, PathBuf};
/// Read every symbol-table name from the object file at `artifact` and demangle
/// it (v0 `_R…` / legacy `_ZN…`) to the readable path `nm -C` prints — pure-Rust
/// via the `object` parser + `rustc-demangle`, no `nm` subprocess. Reads
/// `.symtab` (like plain `nm`), not the dynamic table, so counts match
/// `nm -C bin | grep -c`.
///
/// `None` ⇒ the audit is UNMEASURED, never a pass, in **three** cases:
///
/// ```text
/// absent the file is not there
/// not an object file an ISO, a tarball, a shell script
/// STRIPPED it parsed, and the symbol table is empty
/// ```
///
/// # Why a stripped binary is unmeasured rather than clean
///
/// This is the silent-green shape the whole module exists to avoid, and it was
/// here until 2026-08-06. A `strip = true` release binary has **no symbols at
/// all**, so every forbidden family counts zero, so a `linkage = "thin"` claim —
/// and a receipt's `gpu = false` — passed *because nothing could be seen*. That
/// is the exact conflation of "no evidence of a problem" with "evidence of no
/// problem" the doc comment above warns about, arrived at by a different road:
/// the reader succeeded, and had nothing to say. An empty symbol table is not a
/// measurement, so it does not get to be a pass.
///
/// Public because [`receipt_proof`](crate::receipt_proof) measures a receipt's
/// claims against the same symbol table this audit reads. It consumes THIS
/// function rather than carrying its own `object` walk: a second symbol read
/// would be a second answer to "what does this binary link", and the two would
/// disagree the first time one of them learned about a mangling or a section the
/// other did not — starting with this rule.
pub fn demangled_symbols(artifact: &Path) -> Option<Vec<String>> {
use object::{Object, ObjectSymbol};
let bytes = std::fs::read(artifact).ok()?;
let file = object::File::parse(&*bytes).ok()?;
let symbols: Vec<String> = file
.symbols()
.filter_map(|s| s.name().ok())
.map(|n| format!("{:#}", rustc_demangle::demangle(n)))
.collect();
if symbols.is_empty() {
return None;
}
Some(symbols)
}
/// Is `sym` a compiler-generated DESTRUCTOR, rather than code somebody wrote?
///
/// `core::ptr::drop_glue::<T>` and `core::ptr::drop_in_place::<T>` are emitted by
/// rustc for a monomorphised `T`, and the demangled name spells out **every type
/// parameter of `T`**. That matters here because the families in
/// [`THIN_FORBIDDEN_SYMBOLS`] are matched as SUBSTRINGS, so destructor glue for a
/// deeply-generic type trips a family it merely *mentions*:
///
/// ```text
/// core::ptr::drop_glue::<hyper_util::service::glue::TowerToHyperService<…
/// <tonic::transport::server::Server>::serve_internal<…>::{closure#0}…>>
/// ```
///
/// One symbol, and it matches `tonic::transport::server` without a single byte of
/// server code being reachable.
///
/// **Why it is safe to exclude glue from the count.** To *run* a server you need the
/// server's own code, and that is not glue. Retained glue with no accompanying
/// non-glue symbol means the type is constructed nowhere — the instantiation
/// survived codegen while its constructing code was optimised away. Measured on
/// holger's two real artifacts on 2026-08-01, same flags, same facett:
///
/// | family | thin `gui` | fat `gui-embedded` |
/// |---|---|---|
/// | `tonic::transport::server` | 1 total — **1 glue, 0 non-glue** | 4 total — **0 glue, 4 non-glue** |
/// | `FastRoutes` | 0 | 19 total — 5 glue, **14 non-glue** |
/// | `LocalHolger` | 0 | 34 total — 7 glue, **27 non-glue** |
/// | `iceberg` | 0 | 5 684 total — **4 102 non-glue** |
/// | `parquet` | 0 | 2 387 total — **1 955 non-glue** |
///
/// The genuinely-embedding artifact has **no glue-only family at all**, so ignoring
/// glue costs nothing in detection, while the thin artifact's single false hit
/// disappears. That is the whole justification, and it is a measurement rather than
/// an argument from first principles.
///
/// **Glue is not discarded, it is reclassified** — see [`LinkageAudit::glue_only`].
/// A family whose only evidence is glue is reported, just not counted as linkage. An
/// audit that silently dropped the evidence would be trading one blind spot for
/// another.
pub fn is_compiler_destructor_glue(sym: &str) -> bool {
sym.contains("core::ptr::drop_glue::<") || sym.contains("core::ptr::drop_in_place::<")
}
/// Is `sym` an object-file **FILE symbol** (ELF `STT_FILE`) — the name of a
/// TRANSLATION UNIT the linker recorded — rather than code somebody wrote?
///
/// The symbol table of a binary that statically links C carries one entry per
/// C source file: `p_x25519.c`, `sha512.c`, `zlib_inflate.c`. `object`'s symbol
/// iterator yields them (plain `nm` hides them behind `-a`), and the families
/// in [`count_families`] are matched as SUBSTRINGS, so a translation-unit name
/// trips a family it merely *spells*.
///
/// **MEASURED 2026-08-22**, and it is why this function exists: an installer
/// option declaring `forbid_symbols = ["x25519"]` — the spelling the design
/// document itself uses — went RED on a binary carrying **no WireGuard client
/// at all**. The two hits were `p_x25519.c` and `p_x25519_asn1.c`, aws-lc
/// translation units pulled in by rustls under `reqwest` under `nornir-git`.
/// Every appliance binary in the constellation links a TLS stack, so that
/// guard could never have gone green — the exact mirror of a guard that can
/// never go red, and just as useless.
///
/// The rule is deliberately narrow so it cannot swallow real code: a Rust path
/// always carries `::`, so a name with no `::` that ends in a C/C++/assembly
/// source extension is a translation unit and nothing else.
///
/// Like glue, this is **reclassified, not discarded** — it lands in
/// [`FamilyCounts::glue_only`], so the evidence stays visible and only stops
/// counting as linkage.
pub fn is_object_file_symbol(sym: &str) -> bool {
if sym.contains("::") {
return false;
}
let lower = sym.to_ascii_lowercase();
[".c", ".cc", ".cpp", ".cxx", ".s", ".asm"].iter().any(|e| lower.ends_with(e)) || is_rust_cgu_file_symbol(sym)
}
/// Is `sym` the FILE symbol of a **Rust codegen unit** —
/// `<crate>.<16 hex>-cgu.<n>` (the `.rcgu.o` object's translation-unit name)?
///
/// **MEASURED 2026-09-15**, and it is why this arm exists: gunnar-server built
/// with its runtime grow engine (`korp-installer`, which depends on
/// `wg-appliance-core`) went RED on the `wireguard = off` audit with
/// `wg_appliance_core ×1`. The one hit was
/// `wg_appliance_core.27049babafb37dc3-cgu.0` — the FILE symbol of a codegen
/// unit the linker kept for THREE symbols, all of them `alloc`/`std` generics
/// (`Arc<mpmc::context::Inner>::drop_slow`, `RawVec<…>::grow_one`,
/// `RawVecInner::finish_grow`) that happened to be INSTANTIATED in that crate
/// and deduplicated across the link: the same code every crate using a std
/// channel instantiates, with `wg_appliance_core` only in the v0 mangling's
/// instantiating-crate suffix, which the demangler rightly drops. Not one byte
/// of the crate's own code was in the binary — the `::` paths the audit
/// matches on (`wg_appliance_core::…`) were absent, and the family tripped on
/// a translation-unit name it merely spells, exactly the C case above.
///
/// Narrow on purpose: no `::`, a 16-hex-digit hash after the first `.`, and
/// `-cgu.<digits>` (optionally `.rcgu.o`) at the end. A real symbol of the
/// family carries its path, and a path carries `::`.
pub fn is_rust_cgu_file_symbol(sym: &str) -> bool {
if sym.contains("::") {
return false;
}
let sym = sym.strip_suffix(".rcgu.o").unwrap_or(sym);
let Some((_, rest)) = sym.split_once('.') else { return false };
let Some((hash, n)) = rest.split_once("-cgu.") else { return false };
hash.len() == 16 && hash.bytes().all(|b| b.is_ascii_hexdigit()) && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())
}
/// Count each family's symbols in `symbols`, split into real linkage and glue-only.
///
/// Returns `(found, glue_only)`. A family lands in `found` when at least one
/// NON-glue symbol names it, carrying that non-glue count. A family with matches
/// that are *all* destructor glue lands in `glue_only` instead — visible, but not
/// treated as linkage. A family with no matches at all appears in neither.
///
/// Pure so it can be tested without building a binary; `of_families` is the only
/// caller, so there is one counting rule and not two.
pub fn count_families(symbols: &[String], families: &[&str]) -> FamilyCounts {
let mut found = Vec::new();
let mut glue_only = Vec::new();
for fam in families {
let mut real = 0usize;
let mut glue = 0usize;
for sym in symbols.iter().filter(|l| l.contains(*fam)) {
// Two classes of evidence that are NOT code the Rust build chose:
// compiler destructor glue for a type constructed nowhere, and the
// FILE symbol of a C translation unit. Both are counted separately
// and reported, never treated as linkage.
if is_compiler_destructor_glue(sym) || is_object_file_symbol(sym) {
glue += 1;
} else {
real += 1;
}
}
if real > 0 {
found.push(((*fam).to_string(), real));
} else if glue > 0 {
glue_only.push(((*fam).to_string(), glue));
}
}
FamilyCounts { found, glue_only }
}
/// The split returned by [`count_families`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FamilyCounts {
/// Families present as real, non-glue linkage, as `(family, non-glue count)`.
pub found: Vec<(String, usize)>,
/// Families whose ONLY matches were **not code the Rust build chose** —
/// compiler destructor glue ([`is_compiler_destructor_glue`]) or the FILE
/// symbol of a C translation unit ([`is_object_file_symbol`]) — as
/// `(family, count)`. Reported, not counted.
pub glue_only: Vec<(String, usize)>,
}
// The declaration this module audits. Re-exported so a consumer that only wants the
// guard reaches the audit and the shape it takes through ONE path — there is no
// second `ServiceShape` here, this is `variant`'s.
pub use crate::variant::ServiceShape;
/// Symbol families a **thin** artifact must not contain — the server entry points it
/// claims to *reach* rather than embed, and the storage engine it would need in order
/// to become a second writer against the warehouse lock.
///
/// **Deliberately NOT "any symbol mentioning nornir", and that restraint is the whole
/// design.** A thin client legitimately links the thin gRPC CLIENT crates
/// (`nornir-funnel-embed`, `nornir-warehouse-embed`) and the dep-light edda leaves:
/// 2 346 such symbols in dwarves' thin UI are *correct*, as are the hundreds of
/// `holger_server_lib` symbols (`RemoteHolger`, the generated stubs, the shared view
/// types) in a thin holger UI. A guard that failed those would be wrong, would fail
/// every client there is, and would be switched off within a week. What is named here
/// is what only a SERVER needs.
///
/// An app with its own server entry points (holger's `FastRoutes` / `LocalHolger`)
/// adds them via [`LinkageAudit::of_families`] rather than growing this list —
/// per-app names do not belong in the shared constellation set.
pub const THIN_FORBIDDEN_SYMBOLS: &[&str] = &[
// Server entry points.
"nornir_server",
"nornir::server",
"dwarves::server",
"tonic::transport::server",
// The storage engine. A client that links these can open a warehouse in-process,
// which is exactly the second-writer hazard the thin rule exists to prevent.
"iceberg",
"parquet",
"datafusion",
];
/// **The outcome of a linkage audit — five distinct states, because four of them are
/// not "passed".**
///
/// The reason this is an enum and not a `bool` is [`NotMeasured`](Self::NotMeasured):
/// an audit that could not read the artifact must be tellable apart from one that
/// read it and found nothing, at every call site, forever.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkageVerdict {
/// The artifact could not be read — absent, or not a parseable object file.
/// **Never a pass**: an unchecked guarantee is not a guarantee.
NotMeasured,
/// Declared thin, and carries none of the forbidden families. The good case.
ThinAndClean,
/// Declared thin but measurably links a server entry point or the storage engine.
/// The declaration is a lie.
ThinButLinks,
/// Declared fat/mixed and measurably embeds. Expected — a fat build is *supposed*
/// to carry these.
EmbeddedAndEmbeds,
/// Declared fat/mixed but embeds nothing the audit can see. Not a lie about
/// safety, but it makes the paired thin check vacuous, so it does not hold.
EmbeddedButEmpty,
/// [`ServiceShape::NotApplicable`] — the artifact declared out loud that the
/// fat/thin axis does not apply to it (a `lib`, a browser demo with no wire).
/// Holds, because it is a DECLARATION, not a silence: an omitted linkage reads
/// as fat and is audited as fat.
AxisNotApplicable,
}
impl LinkageVerdict {
/// Whether the declaration survived contact with the binary.
pub fn holds(self) -> bool {
matches!(
self,
LinkageVerdict::ThinAndClean
| LinkageVerdict::EmbeddedAndEmbeds
| LinkageVerdict::AxisNotApplicable
)
}
}
/// What a linkage audit found in a built artifact.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkageAudit {
/// The variant audited.
pub variant: String,
/// The binary.
pub artifact: PathBuf,
/// Its size.
pub bytes: u64,
/// The linkage the declaration claims.
pub declared: ServiceShape,
/// Forbidden symbol families actually PRESENT, as `(family, symbol count)`.
/// Families with zero hits are not listed, so `found.len()` is the number of
/// families present.
///
/// The count is **non-glue symbols only** — see [`is_compiler_destructor_glue`].
pub found: Vec<(String, usize)>,
/// Families whose only matches were compiler destructor glue, as
/// `(family, glue count)`. **Reported, never counted as linkage**, and it does
/// not affect [`verdict`](Self::verdict): retained `drop_glue` for a type that is
/// constructed nowhere is a codegen artifact, not an embedded server. Kept
/// visible so the evidence is reclassified rather than discarded — if a family
/// ever shows up here on an artifact you believe embeds, that is worth reading.
pub glue_only: Vec<(String, usize)>,
/// Whether the object parser could read the binary at all. `false` ⇒ UNMEASURED, which is
/// never reported as a pass.
pub measured: bool,
}
impl LinkageAudit {
/// Audit `artifact` against the shared [`THIN_FORBIDDEN_SYMBOLS`].
///
/// `None` when the artifact is ABSENT — a build that was never run is not an
/// audit at all, and the caller decides whether that is "skip, not built" or a
/// hard failure. Everything the audit *can* observe but finds wrong comes back as
/// a `Some` with a non-holding [`verdict`](Self::verdict); in particular an
/// artifact that exists but cannot be read is `Some` + [`LinkageVerdict::NotMeasured`].
pub fn of(variant: &str, declared: ServiceShape, artifact: &Path) -> Option<LinkageAudit> {
Self::of_families(variant, declared, artifact, THIN_FORBIDDEN_SYMBOLS)
}
/// [`of`](Self::of) with an app's OWN symbol families — the shared list plus, say,
/// holger's `FastRoutes` / `LocalHolger` server entry points. Per-app names stay
/// out of the constellation-wide constant; the contract is identical.
pub fn of_families(
variant: &str,
declared: ServiceShape,
artifact: &Path,
families: &[&str],
) -> Option<LinkageAudit> {
let bytes = std::fs::metadata(artifact).ok()?.len();
// Read the symbol table straight out of the ELF with the pure-Rust `object`
// parser and demangle each name with `rustc-demangle` — the in-process
// equivalent of `nm -C`, with NO `nm` subprocess (LAW: symbol reading is
// pure-Rust, mirroring `plugin_host`'s `object::File::parse`). `None` ⇒ the
// artifact could not be parsed as an object file: UNMEASURED, never a pass.
let symbols = demangled_symbols(artifact);
let measured = symbols.is_some();
let symbols = symbols.unwrap_or_default();
// One symbol-table entry is one symbol, so this counts SYMBOLS rather than
// substring occurrences — the same number a human reproduces with
// `nm -C bin | grep -c` (families such as `nornir::server` only match once
// the v0/legacy mangling is undone, which is why we demangle first), MINUS
// compiler destructor glue, which `grep -c` cannot tell apart from code.
let counts =
if measured { count_families(&symbols, families) } else { FamilyCounts::default() };
Some(LinkageAudit {
variant: variant.to_string(),
artifact: artifact.to_path_buf(),
bytes,
declared,
found: counts.found,
glue_only: counts.glue_only,
measured,
})
}
/// The verdict. See [`LinkageVerdict`] — "could not measure" is one of the
/// outcomes, and it is not a pass.
pub fn verdict(&self) -> LinkageVerdict {
if !self.measured {
return LinkageVerdict::NotMeasured;
}
match self.declared {
ServiceShape::NotApplicable => LinkageVerdict::AxisNotApplicable,
ServiceShape::ThinStandalone => {
if self.found.is_empty() {
LinkageVerdict::ThinAndClean
} else {
LinkageVerdict::ThinButLinks
}
}
// Fat and Mixed both claim to compile siblings IN.
ServiceShape::FatService | ServiceShape::Mixed => {
if self.found.is_empty() {
LinkageVerdict::EmbeddedButEmpty
} else {
LinkageVerdict::EmbeddedAndEmbeds
}
}
}
}
/// Whether the declaration holds. An UNMEASURED audit never passes, and a fat
/// variant that embeds nothing does not either — see [`LinkageVerdict`].
pub fn holds(&self) -> bool {
self.verdict().holds()
}
/// The sentence a report prints.
pub fn line(&self) -> String {
match self.verdict() {
LinkageVerdict::NotMeasured => format!(
"{}: linkage NOT MEASURED (no symbol table in {} — absent, not an object file, \
or STRIPPED) — an unchecked claim",
self.variant,
self.artifact.display()
),
LinkageVerdict::AxisNotApplicable => format!(
"{}: linkage not-applicable (declared) — {}",
self.variant,
human_bytes(self.bytes)
),
LinkageVerdict::ThinAndClean => format!(
"{}: declared THIN and carries no server/engine symbols — {} ✓",
self.variant,
human_bytes(self.bytes)
),
LinkageVerdict::ThinButLinks => format!(
"{}: declared THIN but LINKS {} — the declaration is a lie ({})",
self.variant,
self.families(),
human_bytes(self.bytes)
),
LinkageVerdict::EmbeddedAndEmbeds => format!(
"{}: declared fat/mixed and embeds {} — {} ({} forbidden-family symbol \
group(s) present, which is expected)",
self.variant,
self.families(),
human_bytes(self.bytes),
self.found.len()
),
LinkageVerdict::EmbeddedButEmpty => format!(
"{}: declared fat/mixed but EMBEDS NOTHING — {}; whatever thin check this \
is paired with has become vacuous",
self.variant,
human_bytes(self.bytes)
),
}
}
/// `iceberg×5642, parquet×2393` — the families present, for a message.
pub fn families(&self) -> String {
self.found.iter().map(|(s, n)| format!("{s}×{n}")).collect::<Vec<_>>().join(", ")
}
}
/// `1024` → `"1.0 KB"`. Shared so a size in a build report reads the same everywhere.
pub fn human_bytes(n: u64) -> String {
const K: f64 = 1024.0;
let n = n as f64;
if n < K {
format!("{n:.0} B")
} else if n < K * K {
format!("{:.1} KB", n / K)
} else if n < K * K * K {
format!("{:.1} MB", n / (K * K))
} else {
format!("{:.2} GB", n / (K * K * K))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn audit(declared: ServiceShape, found: Vec<(String, usize)>, measured: bool) -> LinkageAudit {
LinkageAudit {
variant: "x".into(),
artifact: PathBuf::from("/nope"),
bytes: 0,
declared,
found,
// Glue-only families never affect the verdict, so every verdict test below
// is written against the default (none) and stays about `found` alone.
glue_only: Vec::new(),
measured,
}
}
/// **The property the whole module exists for.** An audit that could not read the
/// binary is not a passing audit — it is its own outcome, and it says so out loud.
#[test]
fn an_unmeasured_linkage_audit_never_passes() {
for shape in [
ServiceShape::ThinStandalone,
ServiceShape::FatService,
ServiceShape::Mixed,
ServiceShape::NotApplicable,
] {
let a = audit(shape, Vec::new(), false);
assert_eq!(a.verdict(), LinkageVerdict::NotMeasured, "{shape:?}");
assert!(!a.holds(), "an unchecked claim is not a verified one ({shape:?})");
assert!(a.line().contains("NOT MEASURED"), "{}", a.line());
}
}
/// A thin declaration is failed by ANY forbidden family, and passes only on none.
#[test]
fn a_thin_declaration_is_failed_by_what_it_links() {
let clean = audit(ServiceShape::ThinStandalone, Vec::new(), true);
assert_eq!(clean.verdict(), LinkageVerdict::ThinAndClean);
assert!(clean.holds());
let lying = audit(ServiceShape::ThinStandalone, vec![("iceberg".into(), 42)], true);
assert_eq!(lying.verdict(), LinkageVerdict::ThinButLinks);
assert!(!lying.holds());
assert!(lying.line().contains("the declaration is a lie"), "{}", lying.line());
assert!(lying.line().contains("iceberg×42"), "{}", lying.line());
}
/// A fat variant is never failed FOR BEING FAT — the check is directional.
#[test]
fn a_fat_variant_is_not_failed_for_being_fat() {
for shape in [ServiceShape::FatService, ServiceShape::Mixed] {
let fat = audit(shape, vec![("iceberg".into(), 42)], true);
assert_eq!(fat.verdict(), LinkageVerdict::EmbeddedAndEmbeds, "{shape:?}");
assert!(fat.holds(), "{shape:?}");
assert!(fat.line().contains("which is expected"), "{}", fat.line());
}
}
/// …but it IS failed for not being fat. A guard that passes everything proves
/// nothing: if the embedded variant stops embedding, the thin check it anchors is
/// vacuous, and that must be loud rather than green.
#[test]
fn an_embedded_variant_that_embeds_nothing_makes_the_thin_check_vacuous() {
let hollow = audit(ServiceShape::FatService, Vec::new(), true);
assert_eq!(hollow.verdict(), LinkageVerdict::EmbeddedButEmpty);
assert!(!hollow.holds());
assert!(hollow.line().contains("vacuous"), "{}", hollow.line());
}
/// `not-applicable` is a DECLARATION and holds; an omitted linkage is not this —
/// it deserializes as fat and is audited as fat.
#[test]
fn a_declared_inapplicable_axis_holds_but_silence_is_audited_as_fat() {
let na = audit(ServiceShape::NotApplicable, Vec::new(), true);
assert_eq!(na.verdict(), LinkageVerdict::AxisNotApplicable);
assert!(na.holds());
// Silence: `ServiceShape::default()` is fat, so an undeclared linkage is held
// to the fat contract rather than waved through.
assert_eq!(ServiceShape::default(), ServiceShape::FatService);
assert!(!audit(ServiceShape::default(), Vec::new(), true).holds());
}
/// The forbidden list names server entry points and the storage engine — never a
/// whole product. `nornir`/`holger` as bare words would fail every thin client
/// there is (2 346 legitimate nornir symbols in dwarves' thin UI alone).
#[test]
fn the_forbidden_list_names_entry_points_not_products() {
for s in THIN_FORBIDDEN_SYMBOLS {
assert!(
*s != "nornir" && *s != "holger" && *s != "dwarves" && *s != "skade",
"'{s}' is a whole product, not a server entry point — a guard on it \
would fail every thin client and be switched off within a week"
);
}
for must in ["nornir_server", "dwarves::server", "tonic::transport::server"] {
assert!(THIN_FORBIDDEN_SYMBOLS.contains(&must), "{must} is a server entry point");
}
for engine in ["iceberg", "parquet", "datafusion"] {
assert!(THIN_FORBIDDEN_SYMBOLS.contains(&engine), "{engine} is the storage engine");
}
}
/// A missing artifact is `None` — no audit happened at all, which the caller must
/// distinguish from one that ran.
#[test]
fn a_missing_artifact_is_no_audit_at_all() {
assert!(
LinkageAudit::of("x", ServiceShape::ThinStandalone, Path::new("/nonexistent-artifact"))
.is_none()
);
}
/// The real reader over a real file: the `object` parser on this test binary
/// measures something, and an app's own families ride alongside the shared ones.
#[test]
fn object_reads_a_real_binary_and_extra_families_are_honoured() {
let me = std::env::current_exe().expect("test binary path");
let Some(a) = LinkageAudit::of_families(
"self",
ServiceShape::FatService,
&me,
&["nornir_build_thing", "definitely_not_a_symbol_xyzzy"],
) else {
panic!("the running test binary exists");
};
if !a.measured {
// The binary would not parse as an object file — the audit says NOT
// MEASURED and does not pass.
assert_eq!(a.verdict(), LinkageVerdict::NotMeasured);
assert!(!a.holds());
return;
}
assert!(a.bytes > 0);
assert!(
a.found.iter().any(|(f, n)| f == "nornir_build_thing" && *n > 0),
"this crate's own symbols are in its test binary: {:?}",
a.found
);
assert!(
!a.found.iter().any(|(f, _)| f == "definitely_not_a_symbol_xyzzy"),
"absent families are not listed"
);
}
#[test]
fn human_bytes_reads_like_a_size() {
assert_eq!(human_bytes(512), "512 B");
assert_eq!(human_bytes(2048), "2.0 KB");
assert_eq!(human_bytes(5 * 1024 * 1024), "5.0 MB");
}
/// The REAL symbol that made holger's thin-client guard go red on 2026-08-01,
/// copied verbatim out of `nm -C holger-server-ui`. Not a hand-written
/// approximation: a fixture invented to match the predicate would prove only
/// that the predicate matches its own fixture.
const REAL_GLUE: &str = "core::ptr::drop_glue::<hyper_util::service::glue::TowerToHyperService<tower::util::map_request::MapRequest<tower::util::boxed_clone::BoxCloneService<http::request::Request<tonic::body::Body>, http::response::Response<tonic::body::Body>, alloc::boxed::Box<dyn core::error::Error + core::marker::Send + core::marker::Sync>>, <tonic::transport::server::Server>::serve_internal<tonic::service::router::Routes, tonic::transport::server::incoming::TcpIncoming, core::future::ready::Ready<()>, tokio::net::tcp::stream::TcpStream, std::io::error::Error, tonic::body::Body>::{closure#0}::{closure#1}>>>";
/// A real non-glue symbol from the fat `gui-embedded` artifact's server path.
const REAL_SERVER_CODE: &str =
"<tonic::transport::server::Server>::serve_with_shutdown::<tonic::service::router::Routes, core::future::ready::Ready<()>>";
#[test]
fn a_rust_codegen_unit_file_symbol_is_a_translation_unit_and_not_linkage() {
// The measured hit of 2026-09-15 (gunnar-server + grow-runtime, wireguard off).
assert!(is_rust_cgu_file_symbol("wg_appliance_core.27049babafb37dc3-cgu.0"));
assert!(is_rust_cgu_file_symbol("wg_appliance_core.27049babafb37dc3-cgu.12.rcgu.o"));
assert!(is_object_file_symbol("wg_appliance_core.27049babafb37dc3-cgu.0"));
// Real linkage of the family carries its path, and is still counted.
assert!(!is_rust_cgu_file_symbol("wg_appliance_core::keys::generate_keypair"));
assert!(!is_rust_cgu_file_symbol("wg_appliance_core.notahash-cgu.0"));
assert!(!is_rust_cgu_file_symbol("wg_appliance_core.27049babafb37dc3-cgu."));
let symbols: Vec<String> = ["wg_appliance_core.27049babafb37dc3-cgu.0", "<alloc::sync::Arc<std::sync::mpmc::context::Inner>>::drop_slow"].iter().map(|s| s.to_string()).collect();
let counts = count_families(&symbols, &["wg_appliance_core"]);
assert!(counts.found.is_empty(), "{counts:?}");
assert_eq!(counts.glue_only, vec![("wg_appliance_core".to_string(), 1)]);
let real: Vec<String> = vec!["wg_appliance_core::keys::generate_keypair".into()];
assert_eq!(count_families(&real, &["wg_appliance_core"]).found, vec![("wg_appliance_core".to_string(), 1)]);
}
#[test]
fn destructor_glue_is_recognised_and_ordinary_code_is_not() {
assert!(is_compiler_destructor_glue(REAL_GLUE), "the real drop_glue symbol");
assert!(is_compiler_destructor_glue(
"core::ptr::drop_in_place::<tonic::transport::server::Server>"
));
assert!(!is_compiler_destructor_glue(REAL_SERVER_CODE), "server code is not glue");
// The word appearing in a NAME is not the same as being glue.
assert!(!is_compiler_destructor_glue("holger_server_lib::drop_glue_helper"));
}
#[test]
fn a_family_seen_only_as_destructor_glue_is_reported_but_not_counted() {
let syms = vec![REAL_GLUE.to_string()];
let c = count_families(&syms, &["tonic::transport::server"]);
assert!(
c.found.is_empty(),
"glue alone is not linkage — this is the false red that failed holger's thin \
`gui` tier on 2026-08-01, where the artifact carried 0 FastRoutes, 0 LocalHolger \
and 0 iceberg/parquet symbols. found={:?}",
c.found
);
assert_eq!(
c.glue_only,
vec![("tonic::transport::server".to_string(), 1)],
"and it must still be REPORTED — reclassified, not discarded"
);
}
#[test]
fn real_server_code_is_still_caught_even_beside_glue() {
let syms = vec![REAL_GLUE.to_string(), REAL_SERVER_CODE.to_string()];
let c = count_families(&syms, &["tonic::transport::server"]);
assert_eq!(
c.found,
vec![("tonic::transport::server".to_string(), 1)],
"one NON-glue symbol; the glue beside it must not inflate the count"
);
assert!(c.glue_only.is_empty(), "the family is real linkage, so it is not glue-only");
}
/// The floor that stops this from becoming a way to pass by having no symbols.
#[test]
fn the_counter_is_not_vacuous_and_absence_is_distinguishable() {
// Nothing in, nothing out — but the caller learns nothing was found, and
// `of_families` separately records `measured: false` when it could not read
// the file at all, so "clean" and "unread" never collapse.
let c = count_families(&[], &["tonic::transport::server", "iceberg"]);
assert!(c.found.is_empty() && c.glue_only.is_empty());
// A family that is present as ordinary code is counted, so a non-empty
// symbol list genuinely exercises the match.
let c = count_families(&["iceberg::table::Table::scan".to_string()], &["iceberg"]);
assert_eq!(c.found, vec![("iceberg".to_string(), 1)]);
}
}