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

nordisk/nornir

tests/builder2_mimir.rs

//! BUILDER 2 end to end over a real `nornir-server`: a Build Thing run filed the
//! way the forge files it (`Telemetry.SubmitBuildProvenance`, then
//! `Telemetry.SubmitBuildInfo` with the lineage rows) lands in the workspace's
//! Mímir as a queued jera job — and `CodeRoom.Answer` / `Coderoom.Artifacts`
//! answers which artifacts were built from a crate / a commit; the binary half
//! is in `binary_facts` under the artifact's sha256.
//!
//!   cargo test --features viz,server --test builder2_mimir -- --nocapture
#![cfg(all(feature = "viz", feature = "server"))]

use std::io::Write as _;
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::{Child, Command};
use std::time::{Duration, Instant};

use nornir::viz::remote;

const WS: &str = "b2";

// ── ephemeral server (single empty-seeded workspace) ────────────────────────

fn free_port() -> u16 {
    TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port()
}

struct Server {
    child: Child,
    root: PathBuf,
    endpoint: String,
    token: String,
}
impl Drop for Server {
    fn drop(&mut self) {
        let _ = self.child.kill();
        let _ = self.child.wait();
        let _ = std::fs::remove_dir_all(&self.root);
    }
}

/// Spawn a server monitoring ONE workspace whose warehouse starts EMPTY — the
/// submit RPCs are the only thing that ever writes to it, so a non-zero read-back
/// proves the RPC actually persisted.
fn spawn_server() -> Server {
    let token = "b2-submit-token-0123456789";
    let port = free_port();
    let endpoint = format!("http://127.0.0.1:{port}");
    // Unique per spawn: the three tests run in parallel, so a shared root/HOME
    // would collide (same registry + warehouse path).
    static N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
    let uniq = N.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
    let root = std::env::temp_dir().join(format!("nornir-b2-{}-{uniq}", std::process::id()));
    std::fs::create_dir_all(&root).unwrap();
    let server_home = root.join("home");
    let registry_root = server_home.join(".nornir").join("workspaces");
    std::fs::create_dir_all(&registry_root).unwrap();

    // A path-member descriptor: the fetch loop skips path members (no clone), so
    // it registers instantly. The warehouse is created lazily by the server.
    let desc = root.join("ws.toml");
    let mut f = std::fs::File::create(&desc).unwrap();
    writeln!(f, "[workspace]\nname = \"{WS}\"\n").unwrap();
    writeln!(
        f,
        "[repos.\"ndata-core\"]\npath = \"{}\"\n",
        env!("CARGO_MANIFEST_DIR")
    )
    .unwrap();
    f.flush().unwrap();
    drop(f);

    // Registry-only design: the server dropped the `NORNIR_MONITOR`/`--monitor` boot
    // flag (commit 0c240f9) — a workspace is now served iff it's present in the redb
    // registry at `<home>/.nornir/workspaces` at boot. Seed the monitored `ndata` row
    // here, then DROP the handle so the server can take the registry's single-writer
    // lock. The path-member descriptor makes the fetch loop skip cloning, so it
    // serves instantly (the warehouse is still created lazily by the submit RPCs).
    {
        use nornir::registry::{Mode, Registry, Workspace};
        let reg = Registry::open(&registry_root).expect("open registry");
        reg.upsert(&Workspace::new(
            WS.to_string(),
            desc.to_string_lossy().into_owned(),
            Mode::Monitored,
            "3600s".to_string(),
            None,
        ))
        .expect("register ndata workspace");
    } // reg dropped → releases registry.redb before the server boots

    let child = Command::new(env!("CARGO_BIN_EXE_nornir-server"))
        .env("NORNIR_SERVER_ADDR", format!("127.0.0.1:{port}"))
        .env("NORNIR_SERVER_TOKEN", token)
        .env("HOME", &server_home)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn nornir-server");

    let srv = Server {
        child,
        root,
        endpoint: endpoint.clone(),
        token: token.to_string(),
    };

    let deadline = Instant::now() + Duration::from_secs(45);
    let mut last = String::from("no answer yet");
    loop {
        match remote::list_workspaces(&endpoint, token) {
            Ok(ws) if ws.iter().any(|s| s == WS) => break,
            Ok(ws) => last = format!("it lists {ws:?}"),
            Err(e) => last = format!("Workspaces.List failed: {e:#}"),
        }
        assert!(
            Instant::now() < deadline,
            "server never served workspace `{WS}` — {last}"
        );
        std::thread::sleep(Duration::from_millis(300));
    }
    srv
}

// ── real input rows ─────────────────────────────────────────────────────────


const SIB_SHA: &str = "0123456789abcdef0123456789abcdef01234567";

#[test]
fn a_filed_build_thing_run_lands_in_mimir_and_is_asked_back() {
    use nornir_build_thing::build_info::{BuildInfoKind, BuildInfoRow, manifest_rows};
    use nornir_build_thing::provenance::{BuildProvenance, HostInfo, Outcome, Toolchain};
    use nornir_build_thing::variant::{BuildVariant, Profile};

    let srv = spawn_server();
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("app");
    std::fs::create_dir_all(root.join("target/release")).unwrap();
    let art = root.join("target/release/app-server");
    std::fs::copy("/bin/true", &art).unwrap();
    let v = BuildVariant { name: "app-server".into(), package: Some("app".into()), bin: Some("app-server".into()), features: vec!["tls".into()], profile: Profile::Release, ..Default::default() };
    let toolchain = Toolchain { rustc: "rustc 1.92.0".into(), host: "x86_64-unknown-linux-gnu".into(), cargo: "cargo 1.92.0".into(), image: None, image_sha256: None };
    let rec = BuildProvenance::capture(&v, "app", &root, toolchain, HostInfo { cores: 8, memory_bytes: 1 << 30 }, Outcome::Success, &[art]).unwrap();
    let mut rows = manifest_rows(&rec, 1);
    let row = |k: BuildInfoKind, n: &str, val: String| BuildInfoRow::new(&rec.id, 2, k).kv(n, val);
    rows.push(row(BuildInfoKind::Provenance, "repo_root", root.display().to_string()));
    rows.push(row(BuildInfoKind::Provenance, "commit:sib", SIB_SHA.to_string()));
    for c in ["app@0.1.0", "sib@0.2.0", "serde@1.0.200"] {
        rows.push(row(BuildInfoKind::Dep, "crate", c.to_string()));
    }

    // the forge's order: the record, then its rows
    assert_eq!(remote::submit_build_provenance(&srv.endpoint, &srv.token, WS, std::slice::from_ref(&rec)).unwrap(), 1);
    assert_eq!(remote::submit_build_info(&srv.endpoint, &srv.token, WS, &rows).unwrap(), rows.len() as u32);

    // the jera job lands it; ask until the crate edges are there
    let ask = |params: &str| -> serde_json::Value {
        let json = remote::code_room_answer(&srv.endpoint, &srv.token, WS, "Coderoom.Artifacts", params).expect("CodeRoom.Answer");
        serde_json::from_str(&json).unwrap()
    };
    let deadline = Instant::now() + Duration::from_secs(90);
    let last = loop {
        let v = ask(r#"{"crate":"serde"}"#);
        if v["count"] == 1 {
            break v;
        }
        assert!(Instant::now() < deadline, "the build never landed in Mímir: {v}");
        std::thread::sleep(Duration::from_millis(500));
    };
    let hit = &last["artifacts"][0];
    assert_eq!((hit["file"].as_str(), hit["variant"].as_str(), hit["repo"].as_str()), (Some("app-server"), Some("app-server"), Some("app")), "{last}");
    assert_eq!(hit["target"], "x86_64-unknown-linux-gnu:release");
    assert!(hit["provenance"].as_str().unwrap().contains(&rec.id));
    // not a git checkout: its record says `unknown`, which is no commit
    assert_eq!(hit["commits"], serde_json::json!([format!("sib@{SIB_SHA}")]), "{last}");
    assert_eq!(ask(&format!(r#"{{"commit":"sib@{}"}}"#, &SIB_SHA[..9]))["count"], 1, "built from the sibling's commit");
    assert_eq!(ask(r#"{"crate":"tokio"}"#)["state"], "empty_fact");
    eprintln!("Coderoom.Artifacts {{crate: serde}} → {last}");

    // the binary half: bin analyze under the artifact's sha256
    let deadline = Instant::now() + Duration::from_secs(60);
    loop {
        let scan = remote::scan_table(&srv.endpoint, &srv.token, "binary_facts", 50, WS);
        if let Ok(scan) = &scan {
            let c = scan.columns.iter().position(|c| c == "content_sha256").expect("content_sha256 column");
            if scan.rows.iter().any(|r| r[c] == rec.artifacts[0].sha256) {
                break;
            }
        }
        assert!(Instant::now() < deadline, "no binary_facts row for the artifact: {scan:?}");
        std::thread::sleep(Duration::from_millis(500));
    }
}