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