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

nordisk/nornir

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