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

nordisk/nornir

tests/release_pipeline.rs

//! End-to-end release pipeline smoke-test.
//!
//! Creates two trivial cargo crates side-by-side in a tempdir, wires
//! them as a nornir workspace, runs the dep-graph + release pipeline
//! in dry-run mode, then reopens the warehouse and asserts the
//! lineage round-trips with non-empty git SHAs.
//!
//! This deliberately avoids cargo-test-on-holger to stay fast.

use std::path::Path;

use nornir::release::pipeline::{self, PipelineOptions};
use nornir::warehouse::dep_graph;
use nornir::warehouse::iceberg::IcebergWarehouse;
use nornir::workspace::WorkspaceDescriptor;

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 git_init(root: &Path) {
    // Pure-Rust fixture setup via gix — no `git` subprocess anywhere in
    // the crate, tests included. `commit_all` snapshots the worktree and
    // creates the initial commit on a fresh repo.
    nornir::gitio::init(root).unwrap();
    nornir::gitio::commit_all(root, "init").unwrap();
}

#[test]
fn release_pipeline_dry_run_records_lineage() {
    let tmp = tempfile::tempdir().unwrap();

    // ── repo A: produces crate `acme-lib` ───────────────────────────
    let a = tmp.path().join("acme");
    write(
        &a.join("Cargo.toml"),
        r#"[package]
name = "acme-lib"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
path = "src/lib.rs"
"#,
    );
    write(&a.join("src/lib.rs"), "pub fn one() -> u32 { 1 }\n");
    git_init(&a);

    // ── repo B: consumes acme-lib via path dep ─────────────────────
    let b = tmp.path().join("widget");
    write(
        &b.join("Cargo.toml"),
        &format!(
            r#"[package]
name = "widget-lib"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
path = "src/lib.rs"

[dependencies]
acme-lib = {{ path = "{}" }}
"#,
            a.display()
        ),
    );
    write(
        &b.join("src/lib.rs"),
        "pub fn two() -> u32 { acme_lib::one() + 1 }\n",
    );
    git_init(&b);

    // ── workspace descriptor ───────────────────────────────────────
    let toml = format!(
        r#"[workspace]
name = "acme-suite"

[repos.acme]
path = "{}"

[repos.widget]
path = "{}"
"#,
        a.display(),
        b.display()
    );
    let toml_path = tmp.path().join("nornir-workspace.toml");
    std::fs::write(&toml_path, toml).unwrap();
    let desc = WorkspaceDescriptor::load(&toml_path).unwrap();
    let graph = dep_graph::build(&desc).unwrap();

    // sanity: build order must put acme before widget
    let order = graph.build_order().unwrap();
    let ai = order.iter().position(|n| n == "acme").unwrap();
    let bi = order.iter().position(|n| n == "widget").unwrap();
    assert!(ai < bi, "expected acme before widget; got {order:?}");

    // ── persist dep graph + run release ────────────────────────────
    let wh_root = tmp.path().join("warehouse-root");
    let wh = IcebergWarehouse::open(&wh_root).unwrap();
    let dep_snap = wh
        .block_on(dep_graph::record_dep_graph(&wh, "acme-suite", &graph))
        .unwrap();

    let report = wh
        .block_on(pipeline::run_pipeline(
            &wh,
            &desc,
            &graph,
            &PipelineOptions {
                dry_run: true,
                dep_graph_snapshot_id: dep_snap,
                refuse_if_dirty: true,
                progress: None,
                gate_config: None,
            },
        ))
        .unwrap();

    assert_eq!(report.repos.len(), 2);
    for r in &report.repos {
        assert_eq!(r.gate_status, "succeeded_dry_run", "{r:?}");
        assert_eq!(r.git.sha.len(), 40, "expected full 40-char git SHA: {r:?}");
        assert_eq!(r.tests_failed, 0);
    }
    // build_order_idx must be deps-first
    assert_eq!(report.repos[0].repo, "acme");
    assert_eq!(report.repos[1].repo, "widget");

    // ── reopen warehouse, query lineage back ───────────────────────
    drop(wh);
    let wh2 = IcebergWarehouse::open(&wh_root).unwrap();
    let history = wh2
        .block_on(pipeline::query_release_history(&wh2, "acme-suite", None))
        .unwrap();
    assert_eq!(history.len(), 1);
    let h = &history[0];
    assert_eq!(h.release_id, report.release_id);
    assert_eq!(h.repos.len(), 2);
    assert_eq!(h.repos[0].repo, "acme");
    assert_eq!(h.repos[1].repo, "widget");
    // The shipped SHAs are the rollback handle:
    assert_eq!(h.repos[0].git.sha, report.repos[0].git.sha);
    assert_eq!(h.repos[1].git.sha, report.repos[1].git.sha);
    // run_pipeline doesn't capture index/dwarf, so its pins are None — and
    // they must round-trip as None (asserts the pin columns read cleanly).
    assert!(h.repos[0].tantivy_snapshot_id.is_none());
    assert!(h.repos[0].dwarf_snapshot_id.is_none());
}

/// The Urðr time-machine pins (`tantivy_snapshot_id` / `dwarf_snapshot_id`)
/// must survive a write→reopen→read round-trip through `release_lineage`.
/// This guards the fragile arrow column indices (14/15) the capture phase
/// depends on to make a release restorable.
#[test]
fn release_lineage_pins_round_trip() {
    use nornir::release::pipeline::{
        RepoGitState, RepoReleaseRecord, persist_lineage, query_release_history,
    };

    let tmp = tempfile::tempdir().unwrap();
    let wh_root = tmp.path().join("warehouse-root");
    let wh = IcebergWarehouse::open(&wh_root).unwrap();

    let release_id = uuid::Uuid::new_v4();
    let dep_snap = uuid::Uuid::new_v4();
    let records = vec![RepoReleaseRecord {
        repo: "acme".into(),
        build_order_idx: 0,
        git: RepoGitState {
            sha: "a".repeat(40),
            branch: "main".into(),
            dirty: false,
        },
        gate_status: "succeeded".into(),
        tests_passed: 3,
        tests_failed: 0,
        published_versions: vec![],
        tantivy_snapshot_id: Some("tantivy-snap-123".into()),
        dwarf_snapshot_id: Some("dwarf-snap-456".into()),
    }];

    wh.block_on(persist_lineage(
        &wh,
        release_id,
        "acme-suite",
        &dep_snap,
        &records,
        false,
    ))
    .unwrap();

    drop(wh);
    let wh2 = IcebergWarehouse::open(&wh_root).unwrap();
    let history = wh2
        .block_on(query_release_history(&wh2, "acme-suite", None))
        .unwrap();
    assert_eq!(history.len(), 1);
    let r = &history[0].repos[0];
    assert_eq!(r.tantivy_snapshot_id.as_deref(), Some("tantivy-snap-123"));
    assert_eq!(r.dwarf_snapshot_id.as_deref(), Some("dwarf-snap-456"));
}