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

nordisk/gunnar

xtask/tests/both_ways.rs

//! **Gunnar builds BOTH ways, and `cargo xtask ci` proves it on every push** (owner
//! order 2026-10-26: G-BOTH-1 plain cargo, G-BOTH-2 the Build Thing; lane GUN-INSTALL).
//!
//! The network halves are `cargo xtask crates-io-install [--stage]` and
//! `cargo xtask lone-clone [--stage]`; the measured twins are `cargo xtask twins`. These
//! tests hold the wiring and the one manifest fact the E0063 of 2026-10-26 came from.

use std::path::{Path, PathBuf};

fn root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask has a parent")
        .to_path_buf()
}

/// The `version = "…"` of a `name = { version = "…" … }` line in a manifest's text.
fn requirement(manifest: &str, name: &str) -> Option<String> {
    manifest
        .lines()
        .map(str::trim)
        .filter(|l| !l.starts_with('#'))
        .find(|l| l.starts_with(&format!("{name} = ")))
        .and_then(|l| l.split("version = \"").nth(1))
        .and_then(|r| r.split('"').next())
        .map(str::to_string)
}

/// The version the lock holds for a package (the first entry of that name).
fn locked(lock: &str, name: &str) -> Option<String> {
    lock.split("[[package]]")
        .find(|b| b.lines().any(|l| l.trim() == format!("name = \"{name}\"")))
        .and_then(|b| {
            b.lines().find_map(|l| {
                l.trim()
                    .strip_prefix("version = \"")
                    .and_then(|r| r.strip_suffix('"'))
                    .map(str::to_string)
            })
        })
}

#[test]
fn requirement_and_locked_read_what_they_say() {
    assert_eq!(
        requirement(
            "[x]\nnornir-build-thing = { version = \"0.2.5\", default-features = false }\n",
            "nornir-build-thing"
        )
        .as_deref(),
        Some("0.2.5")
    );
    assert_eq!(locked("[[package]]\nname = \"a\"\nversion = \"1.0.0\"\n\n[[package]]\nname = \"b\"\nversion = \"2.0.0\"\n", "b").as_deref(), Some("2.0.0"));
}

/// ★ **The E0063 of 2026-10-26.** gunnar-recipe names every field of edda's
/// `InstallerBuild`, `IsoBuild` and `ContainerBuild` in struct literals (on purpose: a
/// field added upstream gets a decision here). So the recipe compiles against exactly
/// the field set of the version it was written for, and its requirement must say that
/// version: `^0.2.5` let a 0.2.6 that the recipe had been changed for be swapped for
/// 0.2.5, and let the published 0.1.0 (written for 0.2.5) take 0.2.6. The requirement
/// is the version the lock builds.
#[test]
fn the_build_thing_requirement_is_the_version_the_recipe_compiles_against() {
    let manifest = std::fs::read_to_string(root().join("Cargo.toml")).expect("root Cargo.toml");
    let lock = std::fs::read_to_string(root().join("Cargo.lock")).expect("Cargo.lock");
    let req = requirement(&manifest, "nornir-build-thing")
        .expect("[workspace.dependencies] nornir-build-thing");
    let held = locked(&lock, "nornir-build-thing").expect("the lock holds nornir-build-thing");
    assert_eq!(
        req, held,
        "[workspace.dependencies] asks nornir-build-thing \"{req}\" while the lock builds {held}: gunnar-recipe's struct literals name the fields of {held}, so a resolution to any other 0.2.x does not compile (E0063, `cargo install gunnar` on 2026-10-26)"
    );
}

/// ★ The lone clone runs on every `cargo xtask ci` (it was SKIPPED since 2026-10-08).
#[test]
fn the_lone_clone_row_is_not_paused() {
    assert_eq!(
        xtask::paid_guard::LONE_CLONE_PAUSED,
        None,
        "the lone-clone row is still paused: G-BOTH-1 needs it on every push"
    );
}

/// ★ `cargo xtask ci` runs the both-ways steps by name.
#[test]
fn ci_runs_the_both_ways_steps() {
    let main =
        std::fs::read_to_string(root().join("xtask/src/main.rs")).expect("xtask/src/main.rs");
    for step in [
        "lone-clone",
        "lone-clone-staged",
        "crates-io-install",
        "crates-io-install-staged",
        "twins",
    ] {
        assert!(
            main.contains(&format!("ci_step(&mut ran, \"{step}\")")),
            "`cargo xtask ci` has no `{step}` step"
        );
    }
    for verb in [
        "Some(\"crates-io-install\")",
        "Some(\"twins\")",
        "Some(\"lone-clone\")",
    ] {
        assert!(main.contains(verb), "`cargo xtask` has no verb {verb}");
    }
}

/// The README tells a user the line the crates.io step runs.
#[test]
fn the_readme_line_is_the_crates_io_step() {
    let readme = std::fs::read_to_string(root().join("crates/gunnar/README.md"))
        .expect("crates/gunnar/README.md");
    let line = format!(
        "cargo install {} --features {}",
        xtask::both_ways::CRATES_IO_PACKAGE,
        xtask::both_ways::CRATES_IO_FEATURES
    );
    assert!(
        readme.contains(&line),
        "crates/gunnar/README.md does not show `{line}`"
    );
}