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

nordisk/edda

crates/nornir-build-thing/tests/build_dag_gunnar.rs

//! **The edge DAG over gunnar's REAL declaration** — wave 3, gap #4 (design f3).
//!
//! `gunnar-installer` declares `[build.installer.kernel_donor] tunnr/tunnr-kernel`;
//! with `--build kernel { --opt unix=off }` the DAG must print the donor step first
//! (tunnr's kernel, the veto on its line, the pin from tunnr's OWN declaration),
//! then the installer. Reads the sibling checkouts through the constellation layout
//! rule ([`BesideCheckouts`]) — the same rule the load-time edge type check uses.
//! When gunnar or tunnr is not beside edda the test says so by name and passes:
//! a sibling's absence is a fact about this box, not about the model.

use std::path::PathBuf;

use nornir_build_thing::build_dag::{BesideCheckouts, BuildDag, BuildLine, Coordinate, Declarations};
use nornir_build_thing::variant::ArtifactKind;

fn gunnar_root() -> Option<PathBuf> {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../gunnar");
    let root = root.canonicalize().ok()?;
    let tunnr = root.parent()?.join("tunnr");
    if root.join(".nornir/build-variants.toml").is_file() && tunnr.join(".nornir/build-variants.toml").is_file() {
        Some(root)
    } else {
        None
    }
}

#[test]
fn gunnar_installer_with_build_kernel_prints_the_donor_step_first_then_the_installer() {
    let Some(root) = gunnar_root() else {
        eprintln!("SKIP by name: gunnar and/or tunnr is not checked out beside edda — nothing to derive over");
        return;
    };
    let decls = BesideCheckouts::of(&root);
    let target = Coordinate::new("gunnar", "gunnar-installer");
    let line = BuildLine::parse("--build kernel { --opt unix=off } --opt dhcp=off").expect("the grammar");
    let dag = BuildDag::derive(&target, &line, &decls).expect("gunnar's declaration derives");

    // THREE steps since 2026-08-30 (lane AF), and the middle one is the point:
    // `tunnr-kernel` declares its own `toolchain` edge, so the kernel is a
    // CONSUMER as well as a donor and the recursion pulls its toolchain image in
    // ahead of it. Post-order, dependencies first.
    assert_eq!(dag.steps.len(), 3, "{dag}");
    let toolchain = &dag.steps[0];
    assert_eq!(toolchain.coordinate, Coordinate::new("tunnr", "nordisk-kernel-toolchain"));
    assert_eq!(toolchain.typ, ArtifactKind::Container);
    let tools = toolchain.tool_requires().expect("the kernel row requires tools of its toolchain image");
    assert!(tools.rows.contains_key("gcc") && tools.rows.contains_key("qemu-system-x86"), "{:?}", tools.rows.keys().collect::<Vec<_>>());
    assert!(!tools.rows.contains_key("clang"), "tunnr-kernel is a gcc row: {:?}", tools.rows.keys().collect::<Vec<_>>());
    assert_eq!(tools.rows["gcc"][0].1.consumer, "tunnr/tunnr-kernel");
    let tpin = toolchain.pin.as_ref().expect("a container step whose consumers require something carries its pin");
    assert_eq!(tpin.fragment_sha256.len(), 64);

    let donor = &dag.steps[1];
    assert_eq!(donor.coordinate, Coordinate::new("tunnr", "tunnr-kernel"));
    assert_eq!(donor.typ, ArtifactKind::Kernel);
    assert_eq!(donor.line(), "dwarves buildthing tunnr --variant tunnr-kernel --opt kernel.unix=off");
    let pin = donor.pin.as_ref().expect("a kernel step over a real checkout carries its pin");
    assert_eq!(pin.fragment_sha256.len(), 64);
    assert_eq!(pin.ledger_key.len(), 64);
    let tunnr = decls.declaration("tunnr").unwrap();
    assert_eq!(pin.preset, tunnr.require_kernel("tunnr-kernel").unwrap().preset, "the declared preset, no preset flag typed");

    let installer = dag.target().unwrap();
    assert_eq!(installer.coordinate, target);
    assert_eq!(installer.typ, ArtifactKind::Installer);
    assert_eq!(installer.line(), "dwarves buildthing gunnar --variant gunnar-installer --opt kernel.unix=off --opt dhcp=off");
    assert_eq!(
        dag.to_string(),
        "dwarves buildthing tunnr --variant nordisk-kernel-toolchain\n\
         dwarves buildthing tunnr --variant tunnr-kernel --opt kernel.unix=off\n\
         dwarves buildthing gunnar --variant gunnar-installer --opt kernel.unix=off --opt dhcp=off"
    );

    // The same statement in every short form gives the same DAG.
    for spelling in ["--build tunnr { --opt unix=off }", "--build tunnr-kernel --opt kernel.unix=off", "--opt kernel.unix=off --build tunnr/tunnr-kernel"] {
        let l = BuildLine::parse(&format!("{spelling} --opt dhcp=off")).unwrap();
        let d = BuildDag::derive(&target, &l, &decls).unwrap_or_else(|e| panic!("{spelling}: {e:#}"));
        assert_eq!(d.steps[1], dag.steps[1], "{spelling}");
    }

    // A preset choice changes the donor's pin — the step carries the merge it must satisfy.
    let nuked = BuildLine::parse("--build kernel { --opt preset=nuked }").unwrap();
    let d = BuildDag::derive(&target, &nuked, &decls).unwrap();
    assert_eq!(d.steps[1].line(), "dwarves buildthing tunnr --variant tunnr-kernel --opt kernel.preset=nuked");
    assert_eq!(d.steps[1].pin.as_ref().unwrap().preset, "nuked");
    let plain = BuildDag::derive(&target, &BuildLine::parse("--build kernel").unwrap(), &decls).unwrap();
    if pin.preset != "nuked" {
        assert_ne!(d.steps[1].pin, plain.steps[1].pin, "another preset, another merge, another pin");
    }
    // The kernel's PRESET is not the toolchain's business: the tool closure is
    // the same, so the image's step and its pin do not move.
    assert_eq!(d.steps[0].pin, plain.steps[0].pin, "the toolchain image is pinned by the TOOLS, not by the kconfig preset");

    // Without --build: one step, the installer alone (the donor must be pre-built, pin-verified).
    let alone = BuildDag::derive(&target, &BuildLine::parse("--opt dhcp=off").unwrap(), &decls).unwrap();
    assert_eq!(alone.steps.len(), 1);

    // An edge that is not there is refused by name, listing what IS.
    let e = format!("{:#}", BuildDag::derive(&target, &BuildLine::parse("--build wireguard").unwrap(), &decls).unwrap_err());
    assert!(e.contains("no consumption edge") && e.contains("kernel → tunnr/tunnr-kernel : kernel"), "{e}");
}

/// **The bijection over a REAL depth-2 chain** (design f3 · f5, gap 3).
///
/// `tunnr-kernel` declares `[build.kernel.edges.toolchain] tunnr/nordisk-kernel-toolchain
/// : container` — a general edge, on a donor, of a typ that is not the kernel's.
/// Nobody types a `--build` for it: the chain follows it because the kernel row
/// says it needs it. So it is the honest place to prove the rule both ways —
/// `--opt kernel.toolchain=off` takes the image out of the plan, `=on` puts it
/// back, and the kernel step itself is untouched either way. A declaration that
/// cannot fail is not a declaration.
#[test]
fn the_toolchain_edge_leaves_gunnars_plan_when_its_feature_is_off_and_returns_when_it_is_on() {
    let Some(root) = gunnar_root() else {
        eprintln!("SKIP by name: gunnar and/or tunnr is not checked out beside edda — nothing to derive over");
        return;
    };
    let decls = BesideCheckouts::of(&root);
    let target = Coordinate::new("gunnar", "gunnar-installer");
    let coords = |dag: &BuildDag| dag.steps.iter().map(|s| s.coordinate.to_string()).collect::<Vec<_>>();

    let plain = BuildDag::derive(&target, &BuildLine::parse("--build kernel").expect("the grammar"), &decls).expect("derives");
    assert_eq!(coords(&plain), ["tunnr/nordisk-kernel-toolchain", "tunnr/tunnr-kernel", "gunnar/gunnar-installer"]);

    let off = BuildDag::derive(&target, &BuildLine::parse("--build kernel --opt kernel.toolchain=off").unwrap(), &decls).expect("derives");
    assert_eq!(coords(&off), ["tunnr/tunnr-kernel", "gunnar/gunnar-installer"], "{off}");
    assert!(!off.to_string().contains("nordisk-kernel-toolchain"), "{off}");

    let on = BuildDag::derive(&target, &BuildLine::parse("--build kernel --opt kernel.toolchain=on").unwrap(), &decls).expect("derives");
    assert_eq!(coords(&on), coords(&plain), "{on}");
    assert_eq!(on.steps[0].pin, plain.steps[0].pin, "the same image, the same closure, the same pin");
    // The KERNEL step is the same build in all three: the flag is about what
    // the kernel consumes, never about what the kernel IS.
    assert_eq!(off.steps[0].pin, plain.steps[1].pin);
    assert_eq!(on.steps[1].pin, plain.steps[1].pin);
}

/// **The declaration the parallel Njord lane is written against.** `edges_of`
/// and `consumption_edges` are one list in two shapes, and the `kernel_donor`
/// row arrives through the general one as the `kernel` edge of typ kernel —
/// which is what lets a consumer read every edge the same way whichever
/// spelling wrote it.
#[test]
fn gunnars_kernel_donor_reads_back_as_a_general_consumption_edge() {
    let Some(root) = gunnar_root() else {
        eprintln!("SKIP by name: gunnar is not checked out beside edda — nothing to read");
        return;
    };
    let set = BesideCheckouts::of(&root).declaration("gunnar").expect("gunnar's declaration").into_owned();
    let edges = set.consumption_edges("gunnar-installer").expect("a declared variant");
    let kernel = edges.iter().find(|e| e.feature == "kernel").expect("the kernel_donor row IS the kernel edge");
    assert_eq!(kernel.repo, "tunnr");
    assert_eq!(kernel.variant, "tunnr-kernel");
    assert_eq!(kernel.typ, "kernel");
    assert_eq!(kernel.kind(), ArtifactKind::Kernel);
    assert_eq!(kernel.coordinate(), "tunnr/tunnr-kernel");
    // The pair-shaped reader (dwarves' `buildthing` listing) sees the same list.
    let pairs = set.edges_of("gunnar-installer").expect("a declared variant");
    assert_eq!(pairs.len(), edges.len());
    assert_eq!(pairs.iter().map(|(f, _)| f.clone()).collect::<Vec<_>>(), edges.iter().map(|e| e.feature.clone()).collect::<Vec<_>>());
}

/// **`kernel_donor` is untouched, in the toml and in the emission.** The
/// general edge is a second SPELLING, never a migration: gunnar's declaration
/// on disk still says `[build.installer.kernel_donor]`, and re-emitting the set
/// this reader loaded writes that same table back — no `edges.kernel`, which
/// the declaration rules refuse anyway (one spelling per edge). Every published
/// 0.1.x reader, korp-installer's `KERNEL_EDGE_HINT` included, keeps reading
/// exactly what it read before.
#[test]
fn the_kernel_donor_spelling_survives_the_general_edge_verbatim() {
    let Some(root) = gunnar_root() else {
        eprintln!("SKIP by name: gunnar is not checked out beside edda — nothing to read");
        return;
    };
    let on_disk = std::fs::read_to_string(root.join(".nornir/build-variants.toml")).expect("gunnar's declaration");
    assert!(on_disk.contains("[build.installer.kernel_donor]"), "the older spelling is what gunnar declares");
    assert!(!on_disk.contains("edges.kernel"), "and it is the only kernel edge spelling");

    let set = BesideCheckouts::of(&root).declaration("gunnar").expect("gunnar's declaration").into_owned();
    let emitted = toml::to_string(&nornir_build_thing::variant::BuildConfig::emitting(&set)).expect("emits");
    assert!(emitted.contains("[build.installer.kernel_donor]"), "{emitted}");
    assert!(emitted.contains("kernel = \"tunnr-kernel\""), "{emitted}");
    assert!(!emitted.contains("edges.kernel"), "{emitted}");
}