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

nordisk/gunnar

xtask/tests/the_monetize_line.rs

//! **Gunnar's root takes monetize's ONE crates.io line, 0.3.0** (A3, R5 prep; the H20 unblock,
//! /work/handback/p4-rest.md "What would unblock it").
//!
//! Until the owner's publish, gunnar-server and gunnar-grpc asked `monetize-embed = "0.2"`: the
//! 2026-10-04 crate without `fleet.rs`, `sold.rs` and `testing`, while the monetize tree said
//! 0.1.3. monetize 0.3.0 is one line above both, published with monetize-proto and
//! monetize-estate-side (the shared box pump Gunnar's gunnar-repair moves onto in A4) and
//! nordisk-pairing 0.5.27 (the shared duty and re-admission). So:
//!
//! 1. every `monetize-*` requirement of a root member asks the 0.3 line;
//! 2. gunnar-repair names monetize-estate-side with its `pump` (the box pump A4 implements
//!    `PumpBox` for), behind its `fleet` feature;
//! 3. gunnar-server asks nordisk-pairing 0.5.27 or newer;
//! 4. the root takes every `monetize-*` crate from crates.io (G-B7: no sibling patch at the
//!    root), and gunnar-front, the estate workspace that links the root members with
//!    monetize, patches each of them to the monetize sibling in its own manifest.
//!
//! It reads the manifests (xtask carries no TOML parser; a line scanner is enough for the
//! one-line dependency forms this repository writes).

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

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

fn read(rel: &str) -> String {
    std::fs::read_to_string(root().join(rel)).unwrap_or_else(|e| panic!("read {rel}: {e}"))
}

/// The `monetize-*` (and nordisk-pairing) requirements a manifest writes: (name, version).
fn requirements(manifest: &str, prefix: &str) -> Vec<(String, String)> {
    let mut out = Vec::new();
    for line in manifest.lines() {
        let t = line.trim_start();
        if t.starts_with('#') || !t.starts_with(prefix) {
            continue;
        }
        let Some((name, rhs)) = t.split_once('=') else {
            continue;
        };
        let name = name.trim().to_string();
        if name.contains(' ') || name.contains('.') {
            continue;
        }
        let rhs = rhs.trim();
        let version = if rhs.starts_with('"') {
            rhs.split('"').nth(1).unwrap_or("").to_string()
        } else if let Some(at) = rhs.find("version") {
            rhs[at..].split('"').nth(1).unwrap_or("").to_string()
        } else {
            continue; // a path-only or workspace entry carries no requirement of its own
        };
        out.push((name, version));
    }
    out
}

fn triple(v: &str) -> (u64, u64, u64) {
    let mut it = v
        .trim_start_matches(['^', '='])
        .split('.')
        .map(|n| n.parse::<u64>().unwrap_or(0));
    (
        it.next().unwrap_or(0),
        it.next().unwrap_or(0),
        it.next().unwrap_or(0),
    )
}

const ROOT_MEMBERS: [&str; 4] = [
    "crates/gunnar-server/Cargo.toml",
    "crates/gunnar-grpc/Cargo.toml",
    "crates/gunnar-repair/Cargo.toml",
    "crates/gunnar-cli/Cargo.toml",
];

#[test]
fn every_monetize_requirement_of_the_root_asks_the_0_3_line() {
    let mut wrong = Vec::new();
    let mut seen = 0;
    for m in ROOT_MEMBERS {
        for (name, v) in requirements(&read(m), "monetize-") {
            seen += 1;
            let (a, b, _) = triple(&v);
            if (a, b) != (0, 3) {
                wrong.push(format!("{m}: {name} = {v:?}"));
            }
        }
    }
    assert!(
        seen >= 4,
        "found only {seen} monetize requirements in the root members: the scanner checked nothing"
    );
    assert!(
        wrong.is_empty(),
        "a root member asks another monetize line than 0.3:\n  {}",
        wrong.join("\n  ")
    );
}

#[test]
fn gunnar_repair_names_the_shared_box_pump() {
    let m = read("crates/gunnar-repair/Cargo.toml");
    let dep = m
        .lines()
        .find(|l| l.trim_start().starts_with("monetize-estate-side"))
        .unwrap_or_else(|| panic!("gunnar-repair does not name monetize-estate-side"));
    assert!(
        dep.contains("\"pump\""),
        "gunnar-repair takes monetize-estate-side without its pump: {dep}"
    );
    let fleet = m
        .lines()
        .find(|l| l.trim_start().starts_with("fleet ="))
        .expect("gunnar-repair has a `fleet` feature");
    assert!(
        fleet.contains("dep:monetize-estate-side"),
        "the `fleet` feature does not turn the shared pump on: {fleet}"
    );
}

#[test]
fn gunnar_server_asks_nordisk_pairing_0_5_27_or_newer() {
    let reqs = requirements(&read("crates/gunnar-server/Cargo.toml"), "nordisk-pairing");
    assert!(
        !reqs.is_empty(),
        "gunnar-server names no nordisk-pairing requirement"
    );
    for (_, v) in reqs {
        assert!(
            triple(&v) >= (0, 5, 27),
            "gunnar-server asks nordisk-pairing {v:?}: the shared duty and re-admission are 0.5.27"
        );
    }
}

#[test]
fn the_estate_patches_every_monetize_crate_the_root_names() {
    let patches = read("crates/gunnar-front/Cargo.toml");
    let mut missing = Vec::new();
    for m in ROOT_MEMBERS {
        for (name, _) in requirements(&read(m), "monetize-") {
            let line = format!("{name} = {{ path = \"../../../monetize/crates/{name}\" }}");
            if !patches.contains(&line) && !missing.contains(&name) {
                missing.push(name);
            }
        }
    }
    assert!(
        missing.is_empty(),
        "crates/gunnar-front/Cargo.toml does not patch {missing:?} to the monetize sibling"
    );
    let lock = read("Cargo.lock");
    for block in lock.split("[[package]]").skip(1) {
        if block.contains("name = \"monetize-") {
            assert!(
                block.contains("source = \"registry+"),
                "the root lock takes a monetize crate by path:{block}"
            );
        }
    }
}