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

nordisk/nornir

tests/release_gates.rs

//! End-to-end coverage for the release gates `nexus_floor`,
//! `integration_roundtrip` and `docs_fresh` through their *public* API
//! (`nornir::release::gate::*` + `nornir::docs::*`) — the same entry points
//! the MCP, gRPC and CLI surfaces call.
//!
//! The gate functions themselves carry unit tests inline in
//! `src/release/gate.rs`; this file exercises the real cross-module wiring,
//! most importantly `docs_fresh`, whose check lives in the `docs` module and
//! can only be driven with a real `.nornir/` scaffold + rendered artifact on
//! disk.

use std::path::Path;

use nornir::bench::{BenchResult, BenchRun};
use nornir::docs::{self, RepoLayout};
use nornir::release::gate;

fn write(p: &Path, body: &str) {
    if let Some(parent) = p.parent() {
        std::fs::create_dir_all(parent).unwrap();
    }
    std::fs::write(p, body).unwrap();
}

fn result(name: &str, kv: &[(&str, f64)]) -> BenchResult {
    let mut r = BenchResult {
        name: name.into(),
        metrics: Default::default(),
        source: Default::default(),
    };
    for (k, v) in kv {
        r.metrics
            .insert((*k).to_string(), serde_json::Value::from(*v));
    }
    r
}

fn run(results: Vec<BenchResult>) -> BenchRun {
    BenchRun {
        // this fixture is where the run is made, so it mints the id; it did
        // not look at the box
        id: BenchRun::new_id(),
        machine_facts: None,
        date: "2026-01-01".into(),
        timestamp: Some("2026-01-01T00:00:00+00:00".into()),
        version: "0.1.0".into(),
        machine: "ci".into(),
        cores: 1,
        results,
        tests: Vec::new(),
    }
}

// ─── nexus_floor ────────────────────────────────────────────────────────

#[test]
fn nexus_floor_pass_and_fail() {
    // Pass: holger throughput at/above the nexus floor for every result.
    let green = run(vec![result(
        "decode",
        &[("holger_ops_sec", 4200.0), ("nexus_ops_sec", 1000.0)],
    )]);
    assert!(gate::nexus_floor(&green).is_ok());

    // Fail: one result dips below the floor → release-blocking.
    let red = run(vec![result(
        "decode",
        &[("holger_ops_sec", 800.0), ("nexus_ops_sec", 1000.0)],
    )]);
    let err = gate::nexus_floor(&red).unwrap_err().to_string();
    assert!(err.contains("nexus floor"), "got: {err}");
}

// ─── integration_roundtrip ──────────────────────────────────────────────

#[test]
fn integration_roundtrip_pass_and_fail() {
    // Pass: every kind's round-trip closure succeeds.
    let ok = gate::integration_roundtrip(&["blob", "symbol"], |_k| Ok(()));
    assert!(ok.is_ok());

    // Fail: a kind's round-trip fails → error is contextualised with the kind.
    let err = gate::integration_roundtrip(&["blob", "symbol"], |k| {
        if k == "symbol" {
            anyhow::bail!("fetch != publish")
        }
        Ok(())
    })
    .unwrap_err();
    assert!(
        format!("{err:#}").contains("roundtrip failed for symbol"),
        "got: {err:#}"
    );
}

// ─── docs_fresh ─────────────────────────────────────────────────────────

/// Scaffold a `.nornir/README.md` source whose body is fully static (no
/// `nornir:gen` markers) so the rendered output is deterministic and doesn't
/// depend on bench data — keeps the freshness check about render-drift only.
fn scaffold_repo(root: &Path) {
    let layout = RepoLayout::new(root);
    std::fs::create_dir_all(layout.nornir_dir()).unwrap();
    write(
        &layout.source_of("README.md"),
        "# demo\n\nHand-authored source under `.nornir/`.\n",
    );
}

fn ctx_for<'a>(root: &'a Path, run: &'a BenchRun) -> docs::Ctx<'a> {
    // workspace_root == repo_root is fine for a single-repo fixture.
    docs::Ctx::new(root, root, Some(run))
        .with_path_dep_cloner(&nornir::security::docs_path_dep_cloner)
}

#[test]
fn docs_fresh_passes_right_after_render() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    scaffold_repo(root);
    let layout = RepoLayout::new(root);
    let run = run(vec![result("decode", &[("holger_ops_sec", 1.0)])]);

    // Render writes the generated artifact to disk…
    docs::render_all(&layout, &ctx_for(root, &run)).unwrap();
    // …so the freshness gate must now pass (disk == fresh in-memory render).
    docs::render_check_all(&layout, &ctx_for(root, &run))
        .expect("docs_fresh should pass immediately after render");
}

#[test]
fn docs_fresh_fails_when_output_is_stale() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    scaffold_repo(root);
    let layout = RepoLayout::new(root);
    let run = run(vec![result("decode", &[("holger_ops_sec", 1.0)])]);

    docs::render_all(&layout, &ctx_for(root, &run)).unwrap();

    // Tamper with the generated output (drift the source-of-truth vs artifact).
    // The output is read-only after render, so re-grant write first.
    let out = layout.output_of("README.md");
    let mut perms = std::fs::metadata(&out).unwrap().permissions();
    #[allow(clippy::permissions_set_readonly_false)]
    perms.set_readonly(false);
    std::fs::set_permissions(&out, perms).unwrap();
    std::fs::write(&out, "# tampered — out of sync with .nornir/README.md\n").unwrap();

    let err = docs::render_check_all(&layout, &ctx_for(root, &run))
        .expect_err("docs_fresh must fail on stale output");
    let msg = format!("{err:#}");
    assert!(msg.contains("out of date"), "got: {msg}");
}

#[test]
fn docs_fresh_fails_when_source_edited_but_not_re_rendered() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    scaffold_repo(root);
    let layout = RepoLayout::new(root);
    let run = run(vec![result("decode", &[("holger_ops_sec", 1.0)])]);

    docs::render_all(&layout, &ctx_for(root, &run)).unwrap();

    // Edit the SOURCE but forget to re-render → the artifact is now stale.
    write(
        &layout.source_of("README.md"),
        "# demo\n\nNEW paragraph added to the source after the last render.\n",
    );

    let err = docs::render_check_all(&layout, &ctx_for(root, &run))
        .expect_err("editing the source without re-rendering must fail docs_fresh");
    assert!(format!("{err:#}").contains("out of date"), "got: {err:#}");
}

/// Regression: `docs_fresh` (via `assemble_and_check`) must NOT hinge on a
/// bench run existing. A repo with no recorded `BenchRun` (e.g. nornir itself,
/// which ships no `nornir-bench.rs`) used to false-fail the gate with
/// "no bench runs in iceberg …". With `run = None`:
///   - a GENERATED README (carrying the marker) passes outright;
///   - a hand-written README passes too (no version to assert without a run).
/// A hand-written README WITH a run still enforces the `v<version>` headline.
#[test]
fn assemble_and_check_passes_without_a_bench_run() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();

    // 1. Generated README + no run → Ok (the marker short-circuits).
    write(
        &root.join("README.md"),
        &format!("{}\n# demo\n\ngenerated body\n", docs::GENERATED_MARKER),
    );
    docs::assemble_and_check(root, None)
        .expect("generated README must pass docs_fresh with no bench run");

    // 2. Hand-written README (no marker) + no run → Ok (nothing to assert).
    write(
        root.join("README.md").as_path(),
        "# demo\n\nhand-written, no version line\n",
    );
    docs::assemble_and_check(root, None)
        .expect("hand-written README must pass docs_fresh when there is no run to name a version");

    // 3. Hand-written README + a run → still enforces the v<version> headline.
    let r = run(vec![result("decode", &[("holger_ops_sec", 1.0)])]); // version 0.1.0
    let err = docs::assemble_and_check(root, Some(&r))
        .expect_err("hand-written README missing v0.1.0 must fail when a run is present");
    assert!(format!("{err:#}").contains("v0.1.0"), "got: {err:#}");

    write(
        root.join("README.md").as_path(),
        "# demo v0.1.0\n\nnow names the version\n",
    );
    docs::assemble_and_check(root, Some(&r)).expect("hand-written README naming v0.1.0 must pass");
}

// ─── plan #5: release test-DRIVE (tests_gate + run_tests_gate) ──────────────
//
// Two layers, both through the PUBLIC API the MCP/gRPC/CLI gate calls:
//   * `tests_gate` — the pure verdict over already-collected rows (green→Ok,
//     red→Err, regression named). Fast; runs by default.
//   * `run_tests_gate` — the HEAVY end-to-end drive: it actually compiles +
//     runs a fixture crate's matrix, folds with history, and verdicts. Gated
//     `#[ignore]` (heavy: shells cargo) per the heavy-test policy.

use nornir::test_matrix::Aspect;
use nornir::warehouse::test_results::{TestResultRow, status};

#[test]
fn tests_gate_green_pass_red_fail_regression_through_public_api() {
    // Green newest run → Ok.
    let green = vec![
        TestResultRow::unit("g", "fix", "fix", "t::ok", status::PASS, 1.0, 100, ""),
        TestResultRow::unit("g", "fix", "fix", "t::ok2", status::PASS, 1.0, 100, ""),
    ];
    gate::tests_gate(&green).expect("all-green run passes the release tests gate");

    // No run recorded → RED (a release must have exercised its tests).
    assert!(gate::tests_gate(&[]).is_err(), "no run → gate red");

    // Newest run red → Err, and a test green-last-time now failing is flagged a
    // REGRESSION.
    let regressed = vec![
        TestResultRow::unit("g", "fix", "fix", "t::ok", status::PASS, 1.0, 100, ""),
        TestResultRow::unit("r", "fix", "fix", "t::ok", status::FAIL, 1.0, 200, "broke"),
    ];
    let err = gate::tests_gate(&regressed).unwrap_err().to_string();
    assert!(err.contains("RED"), "{err}");
    assert!(err.contains("REGRESSION"), "names the regression: {err}");
    assert!(err.contains("t::ok"), "names the regressed test: {err}");
}

/// Scaffold a minimal cargo crate at `root` whose single unit test passes
/// (`pass=true`) or fails (`pass=false`).
fn scaffold_test_fixture(root: &Path, pass: bool) {
    write(
        &root.join("Cargo.toml"),
        "[package]\nname = \"fix\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[dependencies]\n",
    );
    let body = if pass {
        "#[test]\nfn it_works() { assert_eq!(2 + 2, 4); }\n"
    } else {
        "#[test]\nfn it_works() { assert_eq!(2 + 2, 5); }\n"
    };
    write(&root.join("src/lib.rs"), body);
}

#[test]
#[ignore = "heavy: compiles + cargo-runs a fixture crate's test matrix; use --include-ignored to run"]
fn run_tests_gate_drives_matrix_and_passes_green_fails_red() {
    // Only the Unit aspect keeps the drive fast + toolchain-light (no clippy/fmt/
    // audit required on the box).
    let aspects = vec![Aspect::Unit];

    // GREEN fixture: the drive runs the matrix, the gate passes, and the freshly
    // -run rows come back (so the caller can persist them).
    let green_dir = tempfile::tempdir().unwrap();
    scaffold_test_fixture(green_dir.path(), true);
    let fresh = gate::run_tests_gate(green_dir.path(), &[], &aspects)
        .expect("green fixture passes the release tests gate");
    assert!(!fresh.is_empty(), "the drive produced + returned test rows");
    assert!(
        fresh.iter().any(|r| r.status == status::PASS),
        "the green fixture's passing test is recorded"
    );
    assert!(
        !fresh.iter().any(|r| status::is_red(&r.status)),
        "no red rows in the green run"
    );

    // RED fixture: the same drive must FAIL the gate (the failing test is red).
    let red_dir = tempfile::tempdir().unwrap();
    scaffold_test_fixture(red_dir.path(), false);
    let err = gate::run_tests_gate(red_dir.path(), &[], &aspects)
        .expect_err("red fixture must fail the release tests gate")
        .to_string();
    assert!(err.contains("RED"), "got: {err}");
}