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

nordisk/facett

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