tests/atom_linkage.rs
//! **`linkage` is a claim about the BINARY — this is where nornir's is checked.**
//!
//! `.nornir/build-variants.toml` declares each atom `fat` / `thin` / `mixed`.
//! Nothing in a TOML file makes that true. The build matrix asks for
//! `<x>-ui = thin`; this file is what decides whether nornir can honestly write
//! that word, and on 2026-08-03 the answer was **no, by 76,829 symbols**.
//!
//! # The measurement that settled it (2026-08-03, oden)
//!
//! The LEANEST declared atom, built at exactly its declared flags:
//!
//! ```text
//! cargo build --release --bin nornir-ui --no-default-features --features viz
//! → 268,655,240 bytes
//!
//! datafusion 61,790 nornir::server 0
//! iceberg 8,841 nornir_server 0
//! parquet 6,198 tonic::transport::server 0
//! skade 1,061 LocalHolger 0
//! ```
//!
//! Both halves matter, and only together do they name the shape:
//!
//! * **Not `thin`.** 76,829 symbols across the three families
//! [`THIN_FORBIDDEN_SYMBOLS`] calls the storage engine are in the artifact,
//! because `skade`, `skade-katalog`, `iceberg`, `parquet` and `datafusion` are
//! PLAIN non-optional dependencies of `nornir`. Every nornir binary can open a
//! warehouse in-process. Declaring `thin` would make
//! [`LinkageVerdict::ThinButLinks`] — "the declaration is a lie" — the standing
//! verdict for a shipped atom.
//! * **Not `fat` either.** Zero server entry points. The GUI compiles no server
//! in and reaches holger and dwarves over gRPC.
//!
//! Storage embedded / server reached is precisely [`ServiceShape::Mixed`], which
//! is what the declaration already said — it simply had no evidence behind it
//! until this file existed. For holger's contrasting THIN proof (`LocalHolger` 0,
//! `iceberg` 0, `parquet` 0) see `holger/server/ui/tests/thin_client.rs`; the
//! technique here is that file's, and so is the audit type.
//!
//! # The audit is SHARED, not re-implemented
//!
//! [`LinkageAudit`] and [`THIN_FORBIDDEN_SYMBOLS`] come from
//! `nornir_build_thing::linkage` — the dep-light edda leaf that already owns the
//! `linkage` schema being checked. holger used to carry a private copy of exactly
//! this and deleted it (LAW 5); nornir does not get to grow the twin back. The
//! symbol read is pure-Rust (`object` + `rustc-demangle`), no `nm` subprocess.
//!
//! # Why this guard is not vacuous
//!
//! * **An unmeasured audit never passes.** A missing or unparseable binary is
//! [`LinkageVerdict::NotMeasured`], which is a FAILURE here, never a skip.
//! * **It fails in BOTH directions.** A `mixed`/`fat` atom that stops embedding
//! is [`LinkageVerdict::EmbeddedButEmpty`] and also fails — because at that
//! moment `mixed` has become the wrong word in the other direction and the
//! declaration should be UPGRADED to `thin`. So if someone ever makes skade
//! optional, this goes red demanding the matrix row be honoured, rather than
//! quietly keeping a now-pessimistic label.
//! * **It builds the atom at its DECLARED flags.** Reading
//! `env!("CARGO_BIN_EXE_nornir-ui")` instead would measure the binary cargo
//! built for *this test's* feature unification — nornir's `default` set, which
//! turns nearly everything on — and would therefore find the storage engine in
//! every atom no matter what the declaration said. That is the trap this whole
//! file exists to avoid, so the flags come from the declaration and the build
//! is shelled.
//!
//! Run: `cargo test --test atom_linkage -- --ignored` (heavy — it compiles the
//! atom). [`ci_runs_the_linkage_audit`] is always compiled and goes RED if CI
//! stops invoking it, the same anti-vacuum pairing
//! `tests/build_variants.rs::ci_compiles_the_atom_marker_feature` uses.
use std::path::{Path, PathBuf};
#[path = "support/ci_commands.rs"]
mod ci_commands;
use ci_commands::{ci_commands_listing, ci_runs};
use nornir::build::variant::BuildVariantSet;
use nornir_build_thing::linkage::{LinkageAudit, LinkageVerdict, THIN_FORBIDDEN_SYMBOLS};
use nornir_build_thing::variant::ServiceShape;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn declared() -> BuildVariantSet {
BuildVariantSet::discover(&repo_root())
.expect("read .nornir/build-variants.toml")
.expect("nornir declares a build-variant set")
}
/// A dedicated target dir so the per-atom feature churn never invalidates the
/// normal `target/` (the same reasoning as `xtask feature-matrix`). On oden this
/// must sit on md1, never `/tmp` (RAM) and never `/scratch` (root).
fn target_dir() -> String {
std::env::var("NORNIR_LINKAGE_TARGET_DIR")
.unwrap_or_else(|_| format!("{}/scratch/cargo/nornir-linkage", std::env::var("HOME").unwrap_or_default()))
}
/// Build ONE atom at exactly its declared cargo flags and return its artifact
/// path. The argv is [`BuildVariant::cargo_args`] — the SAME materializer
/// `xtask build` presses and `--list` prints, so this cannot compile something
/// other than what the declaration means.
fn build_atom(name: &str) -> PathBuf {
let set = declared();
let v = set
.get(name)
.unwrap_or_else(|| panic!("atom '{name}' is not declared"));
let args = v.cargo_args();
let out = std::process::Command::new(env!("CARGO"))
.current_dir(repo_root())
.args(&args)
.args(["--target-dir", &target_dir()])
.output()
.unwrap_or_else(|e| panic!("launch cargo for atom '{name}': {e}"));
assert!(
out.status.success(),
"building atom '{name}' failed ({}). cargo {}\n{}",
out.status,
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
let profile = if args.iter().any(|a| a == "release") {
"release"
} else {
"debug"
};
let bin = v.bin.as_deref().unwrap_or(name);
Path::new(&target_dir()).join(profile).join(bin)
}
/// ★ **THE DECLARATION MUST SURVIVE CONTACT WITH THE BINARY.**
///
/// Audits `nornir-ui` — the leanest atom, and the one the build matrix asks to be
/// `thin`. If nornir can ever be thin, it is thin here first; that is why this is
/// the atom worth the compile.
#[test]
#[ignore = "heavy: compiles the nornir-ui atom (~7½ min on oden)"]
fn nornir_ui_linkage_matches_its_declaration() {
let set = declared();
let v = set.get("nornir-ui").expect("nornir-ui is declared");
let declared_shape = v.shape;
let artifact = build_atom("nornir-ui");
let audit = LinkageAudit::of("nornir-ui", declared_shape, &artifact)
.unwrap_or_else(|| panic!("nornir-ui was built but {} is absent", artifact.display()));
// UNMEASURED IS A FAILURE, never a skip.
assert!(
audit.measured,
"could not read the symbol table of {} — an unchecked guarantee is not a \
guarantee, so this is red, not a pass",
artifact.display()
);
let found: Vec<String> = audit
.found
.iter()
.map(|(f, n)| format!("{f} {n}"))
.collect();
assert!(
audit.verdict().holds(),
"nornir-ui declares `linkage = {}` and its BYTES disagree — verdict {:?}. \
Families present: [{}]. \n\
If this went red because the storage engine LEFT the binary (verdict \
EmbeddedButEmpty), the fix is not to silence it: `skade` was made optional \
and the declaration should be UPGRADED to `thin`, which is the build \
matrix row nornir has never been able to honour.",
String::from(declared_shape),
audit.verdict(),
found.join(", ")
);
// ── AND THE SPECIFIC MEASUREMENT, not merely "the verdict holds" ──────────
//
// `verdict().holds()` alone would pass a `mixed` atom that embedded ONE
// family, which is a very different artifact from the one measured on
// 2026-08-03. Pin the two halves that make `mixed` the right word.
let count = |family: &str| -> usize {
audit
.found
.iter()
.find(|(f, _)| f == family)
.map(|(_, n)| *n)
.unwrap_or(0)
};
// Half one: the storage engine IS in there, so `thin` would be a lie.
for engine in ["iceberg", "parquet", "datafusion"] {
assert!(
count(engine) > 0,
"`{engine}` has vanished from nornir-ui. Every nornir binary linked the \
warehouse engine because skade/iceberg/parquet/datafusion are plain \
non-optional deps; if that is no longer true, nornir can finally be \
`thin` and the declaration must say so. Families found: [{}]",
found.join(", ")
);
}
// Half two: NO server is in there, so `fat` would be wrong too. This is the
// half that makes `mixed` a measurement instead of a hedge.
for server in [
"nornir::server",
"nornir_server",
"tonic::transport::server",
] {
assert_eq!(
count(server),
0,
"nornir-ui has started compiling a server in (`{server}`). It is a CLIENT: \
it reaches one. A UI that embeds its own server shows its own in-process \
world and becomes a second writer against the warehouse lock."
);
}
}
/// **The anti-vacuum companion.** The audit above is `#[ignore]`d because it
/// compiles a 268 MB artifact, so a plain `cargo test` never runs it and this
/// file would read green having measured nothing. This one always runs and
/// requires CI to invoke the ignored test — the same pairing
/// `build_variants.rs::ci_compiles_the_atom_marker_feature` and
/// `inotify_watch_wiring.rs::something_in_ci_compiles_the_feature` use.
#[test]
fn ci_runs_the_linkage_audit() {
assert!(
ci_runs("--test atom_linkage"),
"no CI COMMAND runs `--test atom_linkage`, so the one guard that reads \
nornir's binaries instead of believing its TOML never executes and this \
file is green-by-vacuum. Restore the line in .woodpecker.yml (a `#` in front \
of it counts as gone). What CI does run:\n{}",
ci_commands_listing()
);
// ONE command must carry BOTH. The previous form was
// `ci.contains("atom_linkage") && ci.contains("--ignored")` over the whole
// file — satisfied by the two strings sitting on DIFFERENT lines, i.e. by a
// `--ignored` belonging to some other step entirely.
assert!(
ci_commands::ci_commands()
.iter()
.any(|c| c.contains("--test atom_linkage") && c.contains("--ignored")),
"the atom_linkage CI command must itself pass `--ignored` or it runs only this \
companion test — which asserts nothing about any binary. What CI does run:\n{}",
ci_commands_listing()
);
}
/// The shared forbidden list must still name what nornir depends on it naming.
/// If `iceberg`/`parquet` were dropped from `THIN_FORBIDDEN_SYMBOLS` upstream,
/// every assertion above about the storage engine would quietly stop being
/// checked — `audit.found` would simply never mention them.
#[test]
fn the_shared_forbidden_list_still_names_the_storage_engine() {
for engine in ["iceberg", "parquet", "datafusion"] {
assert!(
THIN_FORBIDDEN_SYMBOLS.contains(&engine),
"`{engine}` left nornir_build_thing::linkage::THIN_FORBIDDEN_SYMBOLS, so \
`nornir_ui_linkage_matches_its_declaration` can no longer see it and its \
storage-engine assertions are vacuous"
);
}
assert!(
THIN_FORBIDDEN_SYMBOLS.contains(&"tonic::transport::server"),
"the server entry point left the shared list; the `not fat` half of the audit \
is now vacuous"
);
}
/// Every atom's declared `linkage` must be a value the schema knows — and for
/// nornir, none of them may be `thin` while the storage engine is non-optional.
/// Cheap (no compile), so it runs on every `cargo test` and catches a `thin`
/// written into the TOML long before the heavy audit would.
#[test]
fn no_nornir_atom_claims_thin_while_the_engine_is_mandatory() {
let manifest =
std::fs::read_to_string(repo_root().join("Cargo.toml")).expect("read Cargo.toml");
// The premise, asserted rather than assumed: these are plain deps. If one
// acquires `optional = true`, this test is what tells you the ban can lift.
let engine_is_mandatory = ["skade-katalog = {", "iceberg = {", "parquet = {"]
.iter()
.all(|decl| {
manifest
.lines()
.find(|l| l.trim_start().starts_with(decl))
.map(|l| !l.contains("optional = true"))
.unwrap_or(false)
});
assert!(
engine_is_mandatory,
"a storage-engine dependency became optional — the `thin` ban below no longer \
follows, and nornir may finally be able to honour the matrix's \
`<x>-ui = thin`. Re-measure with `--test atom_linkage -- --ignored` and \
update .nornir/build-variants.toml."
);
for v in &declared().variants {
assert_ne!(
v.shape,
ServiceShape::ThinStandalone,
"atom '{}' declares `linkage = \"thin\"`, which is measurably false: \
skade/iceberg/parquet/datafusion are non-optional, so it links the \
warehouse engine in-process (nornir-ui: 76,829 such symbols, measured \
2026-08-03). `mixed` is the honest word until the engine is made \
optional.",
v.name
);
}
}