xtask/tests/the_pipeline_names_things_that_exist.rs
//! **`facett.runa.ron` may only name variants facett declares and verbs the
//! dispatcher has.**
//!
//! The pipeline is the release chain written down — `bundle → wheel → publish →
//! prove → demo`, five verbs that used to live in a runbook and in somebody's head.
//! Writing it down moves the failure: a `Build(variant: "facet-wrapped-wheel")` or
//! an `Xtask(cmd: "publish-wheel")` is not a typo anybody notices, because the file
//! that contains it is never compiled. It goes red on the forge, in CI, against a
//! checkout nobody is looking at — and it reads as "the release is broken" rather
//! than "the pipeline names something that does not exist".
//!
//! This is the same guard `no_ghost_paths_or_verbs.rs` runs over DOCUMENTS, pointed
//! at the one file that is an instruction to a machine rather than to a reader. The
//! verb scan is deliberately the same shape (read `main.rs`, take every `"x" =>`
//! arm) rather than a second list to maintain.
//!
//! It PARSES rather than greps. `Pipeline::from_ron_str` is the loader the forge
//! uses and it validates on the way in — the stage DAG's cycles and dangling `needs`
//! are checked by the thing that will run it, not by a regex that agrees with it
//! today.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use dwarves_runa::{Pipeline, Step};
use facett_build_thing::{BuildThing, FacettBuildThing};
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf()
}
fn pipeline() -> Pipeline {
let path = repo_root().join("facett.runa.ron");
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} is not readable: {e}", path.display()));
// Parses AND validates: a cycle or a dangling `needs` is refused here, by the
// same loader `dwarves run` uses.
Pipeline::from_ron_str(&src)
.unwrap_or_else(|e| panic!("{} does not load: {e}", path.display()))
}
/// Every verb the dispatcher behind cargo alias `alias` RUNS, read from its `main.rs`
/// rather than listed here — a second list is a list that drifts. An xtask arm that
/// only refuses a verb that moved to facett-ops (`=> moved(`) is not a verb it runs.
fn real_verbs_of(alias: &str) -> BTreeSet<String> {
let main_rs = match alias {
"xtask" => "xtask/src/main.rs",
"ops" => "facett-ops/src/main.rs",
other => panic!("the pipeline names cargo alias `{other}`, and .cargo/config.toml has xtask and ops"),
};
let main = std::fs::read_to_string(repo_root().join(main_rs)).unwrap();
let mut v = BTreeSet::new();
for line in main.lines() {
let t = line.trim();
if t.contains("=> moved(") {
continue;
}
if let Some(rest) = t.strip_prefix('"')
&& let Some((name, tail)) = rest.split_once('"')
&& tail.trim_start().starts_with("=>") && !name.is_empty()
{
v.insert(name.to_string());
}
}
assert!(v.contains(if alias == "ops" { "publish-pip" } else { "wrap-wasm" }), "the verb scan found nothing real: {v:?}");
v
}
#[test]
fn every_build_step_names_a_declared_variant() {
let set = FacettBuildThing.variants();
let declared: Vec<&str> = set.variants.iter().map(|v| v.name.as_str()).collect();
let mut named = 0usize;
for stage in &pipeline().stages {
for step in &stage.steps {
if let Step::Build { variant, .. } = step {
named += 1;
assert!(
declared.contains(&variant.as_str()),
"stage '{}' builds `{variant}`, which facett does not declare. \
Declared: {}",
stage.name,
declared.join(", ")
);
}
}
}
assert!(named >= 2, "the pipeline builds {named} declared variant(s) — the chain starts with the bundle and the wheel, and a pipeline that names neither is not this repo's release");
}
#[test]
fn every_xtask_step_names_a_real_verb() {
let mut named = 0usize;
for stage in &pipeline().stages {
for step in &stage.steps {
if let Step::Xtask { cmd, alias, .. } = step {
named += 1;
let alias = dwarves_runa::xtask_alias(alias);
let verbs = real_verbs_of(alias);
assert!(
verbs.contains(cmd),
"stage '{}' runs `cargo {alias} {cmd}`, which that dispatcher does not \
run. It runs: {}",
stage.name,
verbs.iter().cloned().collect::<Vec<_>>().join(", ")
);
}
}
}
assert!(named >= 3, "the pipeline runs {named} xtask verb(s) — publish-holger, verify-install and test-demo are the three stages cargo cannot do");
}
/// **The chain is in the order the artifacts require**, not merely a set of stages
/// that each happen to work.
///
/// The wheel stages files `wrap-wasm` writes; the index publishes a wheel; the
/// install proof reads that index; the demo suite installs from it. Run out of
/// order every one of those fails in a way that reads like a different bug — which
/// is exactly why the order was worth writing down.
#[test]
fn the_stages_are_chained_in_the_order_the_artifacts_require() {
let p = pipeline();
let order: Vec<&str> = p.stages.iter().map(|s| s.name.as_str()).collect();
assert_eq!(
order,
["bundle", "wheel", "publish", "prove", "demo"],
"the release chain is not the five stages it documents"
);
for (i, stage) in p.stages.iter().enumerate().skip(1) {
let before = p.stages[i - 1].name.as_str();
assert!(
stage.needs.iter().any(|n| n == before),
"stage '{}' does not need '{before}' — an unchained stage can run first, \
and the first thing it would find is a directory the previous stage had \
not written yet",
stage.name
);
}
assert!(
p.stages[0].needs.is_empty(),
"the first stage needs something, so the chain has no entry point"
);
}
/// **The three stages that left xtask run through `cargo ops`.** publish-holger,
/// verify-install and test-demo moved to facett-ops (xtask carries ONE dependency). A
/// step without `alias: "ops"` would run `cargo xtask <cmd>` and hit the moved-verb
/// refusal. This was a pinned KNOWN-RED until dwarves-runa 0.2.4's `Xtask { alias }`
/// reached edda main; now every step naming a moved verb must name the ops door.
#[test]
fn no_stage_points_xtask_at_a_moved_verb_and_the_three_go_through_ops() {
let main = std::fs::read_to_string(repo_root().join("xtask/src/main.rs")).unwrap();
let moved: BTreeSet<String> = main
.lines()
.filter(|l| l.contains("=> moved("))
.filter_map(|l| l.trim().strip_prefix('"')?.split_once('"').map(|(n, _)| n.to_string()))
.collect();
let mut through_ops: Vec<String> = Vec::new();
for stage in &pipeline().stages {
for step in &stage.steps {
if let Step::Xtask { cmd, alias, .. } = step {
let alias = dwarves_runa::xtask_alias(alias);
assert!(
!(alias == "xtask" && moved.contains(cmd)),
"stage '{}' runs `cargo xtask {cmd}`, which xtask REFUSES (moved to facett-ops) — add alias: \"ops\"",
stage.name
);
if alias == "ops" {
through_ops.push(cmd.clone());
}
}
}
}
assert_eq!(through_ops, ["publish-holger", "verify-install", "test-demo"]);
}