xtask/tests/build_verb.rs
//! # `cargo run -p xtask build` — holger's first-party build-tier command
//!
//! Drives the real xtask binary's `build` verb end-to-end and asserts the
//! observable output. `build --suite MVP --list` is holger's ONE product list
//! (HOL-PRODUCTS): the four products (holger, holger-downstream, holger-cli,
//! holger-ui) and the estate's own builds, each with its materialized cargo
//! command; `--features full` goes to the four products only. Each listing emits
//! one `functional_status` row (the `xtask/build`::`build_list` cell) into the
//! matrix sink, exactly like every other xtask verb.
//!
//! LAW: inject a real subprocess run, assert the real stdout + the real emitted
//! row — never merely "it compiled".
use std::path::{Path, PathBuf};
use std::process::Command;
/// Workspace root = the dir above `xtask/`.
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent (workspace root)")
.to_path_buf()
}
/// A private path for this test's `NORNIR_TESTMATRIX_OUT` sink.
///
/// Deliberately NOT `<repo>/target/` — the nordisk build policy pins
/// `target-dir = /home/rickard/cargo-target` in `~/.cargo/config.toml`, so
/// `<repo>/target/` does not exist and the sink writer cannot create a file
/// under it. Both tests here used to hardcode `<repo>/target/…` and therefore
/// failed at the sink-read step with `No such file or directory` on any machine
/// honouring that policy — the stdout assertions passed, so the failure was
/// purely about where the row was asked to land. `temp_dir()` always exists.
fn sink_path(tag: &str) -> PathBuf {
std::env::temp_dir().join(format!("xtask-{tag}-{}.jsonl", std::process::id()))
}
/// Run the xtask binary's `build` verb with `args` (and the testmatrix sink at `sink`).
fn build_verb(args: &[&str], sink: &Path) -> (bool, String, String) {
let out = Command::new(env!("CARGO"))
.args([
"run",
"--quiet",
"--manifest-path",
"xtask/Cargo.toml",
"--features",
"testmatrix",
"--",
"build",
])
.args(args)
.current_dir(workspace_root())
.env("NORNIR_TESTMATRIX_OUT", sink)
.env("NORNIR_TESTMATRIX_REPO", "holger")
.output()
.unwrap_or_else(|e| panic!("spawn cargo run -p xtask build {args:?}: {e}"));
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
/// The sink got the `xtask/build::build_list` row, scoped to repo=holger.
fn assert_list_row(sink: &Path) {
let text = std::fs::read_to_string(sink)
.unwrap_or_else(|e| panic!("read functional sink {}: {e}", sink.display()));
let _ = std::fs::remove_file(sink);
assert!(
text.contains("xtask/build") && text.contains("build_list"),
"the build verb emitted its xtask/build::build_list functional row:\n{text}"
);
assert!(
text.contains("\"repo\":\"holger\""),
"row scoped to repo=holger:\n{text}"
);
}
/// The four products, in the order the docs name them (`holger_recipe::variants::PRODUCTS`).
const PRODUCTS: [&str; 4] = ["holger", "holger-downstream", "holger-ui", "holger-cli"];
/// Every row holger declares: the products, then the estate's own builds.
const ALL_VARIANTS: [&str; 24] = [
"holger",
"holger-downstream",
"holger-cli",
"holger-ui", // the declaration's order
"mvp-server-musl",
"mvp-server-metered",
"mvp-server-musl-metered",
"mvp-server-musl-metered-narrate",
"holger-mvp-appliance",
"holger-installer",
"holger-installer-metered",
"holger-appliance-iso",
"plugin-allocation",
"plugin-skidbladnir",
"plugin-rust-toolchain",
"estate-front-musl-metered",
"estate-front-musl-metered-narrate",
"holger-front-installer-metered",
"holger-keep",
"holger-front-keep",
"downstream-musl",
"holger-downstream-oneway-installer",
"downstream-fetch-musl",
"holger-downstream-installer",
];
/// The first token of every line after the header, kept when it is a declared row name.
fn listed_rows(stdout: &str, header: &str) -> Vec<String> {
stdout
.lines()
.skip_while(|l| !l.contains(header))
.skip(1)
.filter_map(|l| l.split_whitespace().next())
.map(|t| t.trim_end_matches(':').to_string())
.filter(|t| ALL_VARIANTS.contains(&t.as_str()))
.collect()
}
/// `build --suite MVP --list` resolves the ONE product list from the DECLARATION: every
/// row, the four products first, each bin named, and no retired row (the ORIGINAL and OPEN
/// suites, the tier bins, the folded tools).
#[test]
fn build_list_resolves_the_one_product_list_and_emits_a_row() {
let sink = sink_path("build-mvp-list");
let _ = std::fs::remove_file(&sink);
let (ok, stdout, stderr) = build_verb(&["--suite", "MVP", "--list"], &sink);
assert!(
ok,
"build --suite MVP --list must exit 0.\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
assert!(
stdout.contains("delivery 'MVP' — 24 variant(s):"),
"MVP resolves the 24 rows:\n{stdout}"
);
let listed = listed_rows(&stdout, "variant(s):");
let mut dedup = listed.clone();
dedup.dedup();
assert_eq!(
dedup, ALL_VARIANTS,
"every row once, the products first:\n{stdout}"
);
for bin in [
"holger-downstream",
"holger-cli",
"holger-ui",
"holger-mvp",
"holger-mvp-metered",
] {
assert!(
stdout.contains(bin),
"build --list names the '{bin}' bin:\n{stdout}"
);
}
for gone in [
"holger-embedded",
"holger-mikro",
"holger-full",
"holger-bundle",
"holger-verify",
"holger-agent",
] {
assert!(
!stdout.contains(gone),
"the retired `{gone}` is listed:\n{stdout}"
);
}
assert_list_row(&sink);
}
/// The retired deliveries are refused by name.
#[test]
fn the_retired_suites_are_no_delivery() {
let sink = sink_path("build-original");
for gone in ["ORIGINAL", "OPEN"] {
let (ok, stdout, stderr) = build_verb(&["--suite", gone, "--list"], &sink);
assert!(!ok, "`--suite {gone}` must be refused:\n{stdout}");
assert!(
stderr.contains("no such delivery") && stderr.contains("MVP"),
"the refusal names the one delivery:\n{stderr}"
);
}
let _ = std::fs::remove_file(&sink);
}
/// **`--suite MVP --features full`** is the four products with every extra: the features go
/// to the product rows only (the estate's builds, the media and the wasm guests are
/// declared builds and are never re-featured), and `--list` shows exactly what it builds.
#[test]
fn the_products_with_full_are_the_four_products_each_with_full() {
let sink = sink_path("build-full-list");
let _ = std::fs::remove_file(&sink);
let (ok, stdout, stderr) =
build_verb(&["--suite", "MVP", "--features", "full", "--list"], &sink);
assert!(ok, "build --suite MVP --features full --list must exit 0.\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}");
assert!(
stdout.contains("the products with `--features full` — 4 variant(s):"),
"{stdout}"
);
let lines: Vec<&str> = stdout
.lines()
.filter(|l| l.starts_with(" ") && l.contains(": cargo "))
.collect();
let names: Vec<&str> = lines
.iter()
.map(|l| l.trim_start().split(':').next().unwrap())
.collect();
assert_eq!(
names, PRODUCTS,
"the four products and nothing else:\n{stdout}"
);
for l in &lines {
assert!(
l.contains("--features full"),
"each product carries `full`: {l}"
);
assert!(
!l.contains("--no-default-features"),
"on top of its defaults: {l}"
);
}
assert_list_row(&sink);
}
/// HEAVY: `build --variant holger-downstream` shells the product's build in the pinned
/// container and prints the assembled attestation. Ignored by default (a release build).
#[test]
#[ignore = "heavy: a release build of holger-downstream in the toolchain container"]
fn build_variant_presses_a_product_end_to_end() {
let sink = sink_path("build-downstream");
let (ok, stdout, stderr) =
build_verb(&["--suite", "MVP", "--variant", "holger-downstream"], &sink);
let _ = std::fs::remove_file(&sink);
assert!(ok, "build --variant holger-downstream must exit 0.\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}");
assert!(
stdout.contains("building variant 'holger-downstream'"),
"{stdout}"
);
assert!(
stdout.contains("digest :"),
"prints the attestation digest:\n{stdout}"
);
assert!(
stdout.contains("holger-downstream ("),
"hashes the real holger-downstream binary:\n{stdout}"
);
}
/// HEAVY: `build --suite MVP --features full` builds the four products with everything.
#[test]
#[ignore = "heavy: four release builds with every extra (holger-ui needs ../holger-ui beside)"]
fn the_products_build_with_full() {
let sink = sink_path("build-full");
let (ok, stdout, stderr) = build_verb(&["--suite", "MVP", "--features", "full"], &sink);
let _ = std::fs::remove_file(&sink);
assert!(ok, "build --suite MVP --features full must exit 0.\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}");
for p in PRODUCTS {
assert!(
stdout.contains(&format!("building variant '{p}'")),
"{p}:\n{stdout}"
);
}
assert!(
stdout.contains("the four products with `--features full` assembled"),
"{stdout}"
);
}
/// **An unknown verb runs nothing.** Until 2026-10-20 any verb the dispatch did
/// not match (a typo, a retired verb such as `build-mvp`) fell through to the
/// whole local suite: wasm build, `cargo test --workspace` and a 500 000-crate
/// bench. Now it prints the verb list and exits 2, and so does no verb at all.
#[test]
fn an_unknown_verb_prints_the_verbs_and_exits_2() {
let root = workspace_root();
for argv in [&["build-mvp"][..], &["tset"][..], &[][..]] {
let t0 = std::time::Instant::now();
let out = Command::new(env!("CARGO"))
.args([
"run",
"--quiet",
"--manifest-path",
"xtask/Cargo.toml",
"--",
])
.args(argv)
.current_dir(&root)
.output()
.expect("spawn cargo run -p xtask");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(
out.status.code(),
Some(2),
"{argv:?} must exit 2:\n{stdout}\n{stderr}"
);
let text = format!("{stdout}{stderr}");
for verb in ["test", "build", "gate", "rows-check", "bench", "gen-proto"] {
assert!(
text.contains(&format!(" {verb} ")),
"{argv:?}: the verb list names `{verb}`:\n{text}"
);
}
assert!(
!text.contains("=== workspace tests") && !text.contains("=== wasm"),
"{argv:?} ran a suite:\n{text}"
);
assert!(
t0.elapsed() < std::time::Duration::from_secs(120),
"{argv:?} took {:?}: it ran something",
t0.elapsed()
);
}
}