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(®istry_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(®istry_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));
}
}