ci/verbs/tests/runner_uses_detached_verbs.rs
//! The runner's lock check and status page come from this detached crate, never from
//! dwarves' workspace xtask: a sibling's version move that breaks dwarves' graph must not
//! freeze STATUS.md (2026-10-24: frozen at 13:11Z for ten hours, the cron running on).
use std::path::Path;
fn runner() -> String {
let p = Path::new(env!("CARGO_MANIFEST_DIR")).join("../runner.sh");
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
fn code_lines(s: &str) -> Vec<&str> {
s.lines().filter(|l| !l.trim_start().starts_with('#')).collect()
}
#[test]
fn the_runner_writes_the_status_page_and_the_lock_check_with_the_detached_verbs() {
let script = runner();
let code = code_lines(&script);
for verb in ["status", "lockcheck"] {
assert!(
!code.iter().any(|l| l.contains(&format!("xtask ci {verb}")) || (l.contains("run_verb dwarves") && l.contains(verb))),
"the runner still runs `{verb}` through dwarves' workspace xtask"
);
assert!(
code.iter().any(|l| l.contains(&format!("cargo run --quiet --manifest-path \"$VERBS_MANIFEST\" -- {verb} "))),
"the runner does not run `{verb}` from ci/verbs"
);
}
assert!(code.iter().any(|l| l.trim() == "VERBS_MANIFEST=$W/src/dwarves/ci/verbs/Cargo.toml"), "VERBS_MANIFEST is not ci/verbs");
}
#[test]
fn the_crate_is_detached_and_links_no_dwarves_crate() {
let m = std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")).unwrap();
assert!(m.lines().any(|l| l.trim() == "[workspace]"), "ci/verbs is not its own workspace");
let deps = m.split("[dependencies]").nth(1).unwrap_or("");
let names: Vec<&str> = deps.lines().filter_map(|l| l.split('=').next()).map(str::trim).filter(|n| !n.is_empty() && !n.starts_with('#')).collect();
// nornir-xtask-support, serde_json for the nightly Scorecard step's JSON and ureq for
// the nightly OpenSSF badge copy (BADGE-7): crates.io crates, so no sibling and no
// dwarves crate can freeze them
assert_eq!(names, ["nornir-xtask-support", "serde_json", "ureq"], "ci/verbs links more than nornir-xtask-support + serde_json + ureq");
for leaf in ["serde_json", "ureq"] {
assert!(!deps.lines().any(|l| l.starts_with(leaf) && l.contains("path")), "{leaf} comes from crates.io");
}
}
#[test]
fn the_status_page_is_written_from_the_runs_alone() {
let dir = std::env::temp_dir().join(format!("ci-verbs-status-{}", std::process::id()));
let run = dir.join("runs/20261024T230001Z/holger");
std::fs::create_dir_all(&run).unwrap();
std::fs::write(
run.join("receipt.json"),
r#"{"schema": "nornir-ci/1", "repo": "holger", "head": "55ed5ed3aaaa", "mode": "quick", "finished_unix": 1792890000, "green": true, "failing_step": null, "steps": []}"#,
)
.unwrap();
let out = dir.join("STATUS.md");
let args: Vec<String> = ["--runs", dir.join("runs").to_str().unwrap(), "--out", out.to_str().unwrap()].iter().map(|s| s.to_string()).collect();
assert_eq!(ci_verbs::status_verb(&args), 0);
let md = std::fs::read_to_string(&out).unwrap();
assert!(md.contains("| holger | quick | 55ed5ed3"), "{md}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_runner_copies_itself_only_under_its_lock() {
// The cron starts a runner every 5 minutes. A start that copied runner.sh over
// .runner-copy.sh BEFORE it found the lock taken overwrote the script a running run
// was still reading (bash reads a script as it goes): 2026-10-24 23:30, mid-run.
let script = runner();
let code = code_lines(&script);
let lock = code.iter().position(|l| l.contains("flock -n 9")).expect("the runner takes its lock");
let copy = code.iter().position(|l| l.contains(".runner-copy.sh")).expect("the runner runs from a copy");
assert!(lock < copy, "the runner copies itself before it holds its lock");
let open = code.iter().position(|l| l.contains("exec 9>")).expect("the lock's descriptor");
assert_eq!(code.iter().filter(|l| l.contains("exec 9>")).count(), 1, "the copy must keep the inherited lock, not open a second one");
assert!(open < copy);
}