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

nordisk/gunnar

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:?}"
        );
    }
}