crates/mimir-ingest/tests/ingest/product.rs
//! BUILDER 2: a Build Thing run's products in the log — artifact node,
//! BUILT_FROM edges, provenance — in the binary stream's own tables under the
//! pseudo repo `@build-thing`, and the question they answer: which artifacts
//! were built from crate X / commit Y.
use std::path::Path;
use super::common::*;
use mimir_ingest::product::{ArtifactQuery, PRODUCTS_REPO, ProductArtifact, ProductFacts, artifacts};
use mimir_ingest::{Consumer, Kind, RepoSpec};
const GUNNAR_SHA: &str = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678";
const EDDA_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
fn facts() -> ProductFacts {
ProductFacts {
build_id: "build-1".into(),
repo: "gunnar".into(),
variant: "gunnar-server".into(),
package: Some("gunnar-server".into()),
features: vec!["tls".into()],
profile: "release".into(),
triple: "x86_64-unknown-linux-gnu".into(),
toolchain: "rustc 1.92.0".into(),
builder: "builder@dwarf".into(),
built_at: "2026-10-15T20:00:00Z".into(),
recipe: Some("gunnar-build-thing:0.1.0:abcd".into()),
commits: [("gunnar".to_string(), GUNNAR_SHA.to_string()), ("edda".to_string(), EDDA_SHA.to_string())].into(),
crates: vec![("gunnar-server".into(), "0.5.0".into()), ("serde".into(), "1.0.200".into()), ("jera".into(), "0.2.0".into())],
artifacts: vec![
ProductArtifact { file: "gunnar-server".into(), bytes: 1234, sha256: "f".repeat(64) },
ProductArtifact { file: "gunnar-ctl".into(), bytes: 99, sha256: "e".repeat(64) },
],
record: Some(serde_json::json!({ "id": "build-1" })),
}
}
async fn boot(dir: &Path) -> Consumer<FakeDumper> {
let log = open_log(&dir.join("lake")).await;
Consumer::boot(log, FakeDumper { binary: true, ..Default::default() }).await.unwrap()
}
fn q(krate: Option<&str>, commit: Option<&str>) -> ArtifactQuery {
ArtifactQuery { krate: krate.map(str::to_string), commit: commit.map(str::to_string), variant: None }
}
#[tokio::test(flavor = "current_thread")]
async fn a_build_thing_run_lands_its_artifacts_built_from_edges_and_provenance() {
let tmp = tempfile::tempdir().unwrap();
let mut c = boot(tmp.path()).await;
let rows = c.ingest_products(&facts()).await.unwrap();
assert_eq!(rows.len(), 2, "one ledger row per artifact");
assert!(rows.iter().all(|r| r.repo == PRODUCTS_REPO && r.status == "ok" && r.sha == GUNNAR_SHA && r.rows_plus > 0));
// the provenance rides the ledger
let ledger: serde_json::Value = serde_json::from_str(rows[0].ledger.as_deref().unwrap()).unwrap();
assert_eq!(ledger["variant"], "gunnar-server");
assert_eq!(ledger["commits"]["edda"], EDDA_SHA);
assert_eq!(ledger["builder"], "builder@dwarf");
assert_eq!(ledger["record"]["id"], "build-1");
// the binary stream's tables, nothing else
let nodes = events(c.log(), Kind::BinNodes).await;
assert_eq!(nodes.len(), 2);
assert!(nodes.iter().all(|e| e.0 == PRODUCTS_REPO && e.3 == "+"));
let edges = events(c.log(), Kind::BinEdges).await;
// per artifact: 3 crates + 1 feature + 2 commits
assert_eq!(edges.len(), 12, "{edges:#?}");
assert!(edges.iter().all(|e| e.5.ends_with("BUILT_FROM")));
assert!(events(c.log(), Kind::Nodes).await.is_empty(), "no source-stream event");
// the questions
let all = artifacts(c.log(), &ArtifactQuery::default()).await.unwrap();
assert_eq!(all.iter().map(|h| h.file.as_str()).collect::<Vec<_>>(), vec!["gunnar-ctl", "gunnar-server"]);
let h = &all[1];
assert_eq!((h.repo.as_str(), h.variant.as_str(), h.target.as_str()), ("gunnar", "gunnar-server", "x86_64-unknown-linux-gnu:release"));
assert_eq!(h.features, vec!["tls"]);
assert_eq!(h.crates, vec!["gunnar-server@0.5.0", "jera@0.2.0", "serde@1.0.200"]);
assert_eq!(h.commits, vec![format!("edda@{EDDA_SHA}"), format!("gunnar@{GUNNAR_SHA}")]);
assert!(h.provenance.contains("builder@dwarf") && h.provenance.contains("build-1"), "{}", h.provenance);
assert_eq!(artifacts(c.log(), &q(Some("jera"), None)).await.unwrap().len(), 2, "built from crate jera");
assert_eq!(artifacts(c.log(), &q(Some("serde@1.0.200"), None)).await.unwrap().len(), 2);
assert!(artifacts(c.log(), &q(Some("serde@1.0.199"), None)).await.unwrap().is_empty());
assert!(artifacts(c.log(), &q(Some("tokio"), None)).await.unwrap().is_empty());
assert_eq!(artifacts(c.log(), &q(None, Some("0123456"))).await.unwrap().len(), 2, "built from commit 0123456");
assert_eq!(artifacts(c.log(), &q(None, Some(&format!("edda@{}", &EDDA_SHA[..10])))).await.unwrap().len(), 2);
assert!(artifacts(c.log(), &q(None, Some("gunnar@0123456"))).await.unwrap().is_empty(), "that sha is edda's, not gunnar's");
assert!(artifacts(c.log(), &q(None, Some("01234"))).await.unwrap().is_empty(), "a prefix under 7 is no commit");
}
#[tokio::test(flavor = "current_thread")]
async fn the_same_artifacts_again_write_no_event_and_a_members_consume_leaves_them_open() {
let tmp = tempfile::tempdir().unwrap();
let mut c = boot(tmp.path()).await;
c.ingest_products(&facts()).await.unwrap();
let before = (events(c.log(), Kind::BinNodes).await, events(c.log(), Kind::BinEdges).await);
let again = c.ingest_products(&facts()).await.unwrap();
assert_eq!(again.iter().map(|r| (r.rev, r.rows_plus, r.rows_minus)).collect::<Vec<_>>(), vec![(2, 0, 0), (2, 0, 0)]);
assert_eq!((events(c.log(), Kind::BinNodes).await, events(c.log(), Kind::BinEdges).await), before, "idempotent");
// a member with a binary layer consumes: its binary release must not
// close the products (they are no bin of its workspace)
let src = Scratch::new(tmp.path().join("src"));
src.write("Cargo.toml", "[workspace]\nmembers = [\"crates/*\"]\n").write("crates/a/src/lib.rs", "fn f () | one\nlink f\n");
src.commit("c1");
let rs = RepoSpec { name: "member".into(), source: src.root.to_string_lossy().into_owned(), branch: "main".into(), worktree: tmp.path().join("clone"), initial_offset: None };
c.run(&rs).await.unwrap();
assert_eq!(artifacts(c.log(), &ArtifactQuery::default()).await.unwrap().len(), 2, "still open after a member's release");
assert!(events(c.log(), Kind::BinNodes).await.iter().filter(|e| e.0 == PRODUCTS_REPO).all(|e| e.3 == "+"));
// a consumer booted again folds the products from the log: the same build
// once more is still no event
drop(c);
let mut c = Consumer::boot(open_log(&tmp.path().join("lake")).await, FakeDumper { binary: true, ..Default::default() }).await.unwrap();
let n = events(c.log(), Kind::BinEdges).await.len();
let third = c.ingest_products(&facts()).await.unwrap();
assert!(third.iter().all(|r| r.rows_plus == 0 && r.rows_minus == 0));
assert_eq!(events(c.log(), Kind::BinEdges).await.len(), n);
}
#[tokio::test(flavor = "current_thread")]
async fn a_run_with_no_artifact_is_refused() {
let tmp = tempfile::tempdir().unwrap();
let mut c = boot(tmp.path()).await;
let mut f = facts();
f.artifacts.clear();
let e = c.ingest_products(&f).await.unwrap_err().to_string();
assert!(e.contains("no artifact"), "{e}");
}