Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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