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

nordisk/facett

xtask/tests/no_ghost_paths_or_verbs.rs

//! **Row 9 — documents that tell you to run commands which do not exist.**
//!
//! `facett-streamlit` and `facett-embed` are crates that were removed. `cargo xtask
//! release-streamlit` and `release-embed` are verbs that were removed with them. The
//! documents naming them were not, so this repo still contains a **runbook** whose
//! step 2 is a command the binary does not have, and a `.gitignore` guarding paths
//! that cannot exist.
//!
//! That is worse than an out-of-date doc. Someone following
//! `python-packaging-publish-runbook.md` runs `cargo xtask release-streamlit`, gets
//! `unknown command`, and has no way to tell whether they typed it wrong, are on the
//! wrong branch, or are reading a document about a world that ended. The real path is
//! `wrap-wasm` → `publish-pip` → `publish-holger`.
//!
//! Two guards, because the dead NAMES and the dead VERBS rot independently:
//!
//! 1. no file may mention a removed crate, except the ones that are explicitly
//!    history — and each exception is listed with its reason and is itself checked to
//!    still exist, so the allowlist cannot outlive what it excuses;
//! 2. every `cargo xtask <verb>` that a document instructs the reader to run must be
//!    a verb the dispatcher actually has.
//!
//! (2) is the one that generalises: it will catch the next removed verb without
//! anyone remembering to add it here.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf()
}

/// Names of crates and verbs that no longer exist anywhere in this workspace.
const GHOSTS: [&str; 3] = ["facett-streamlit", "facett-embed", "release-embed"];

/// Files whose mention of a ghost is HISTORY and is correct.
///
/// Each entry is a path plus why it is excused. They are checked to still exist: an
/// allowlist that outlives its files silently starts excusing nothing, and then the
/// next person to add a ghost lands in a gap.
const HISTORY: [(&str, &str); 3] = [
    (
        ".nornir/facett-deduplication-history.md",
        "a history of what was merged away — naming the dead crate IS the content",
    ),
    (
        ".nornir/docs/test-matrix-inventory-history.md",
        "a dated inventory; rewriting it would falsify a record",
    ),
    (
        ".nornir/facett-embed-data-seam-design.md",
        "the design doc OF the removed crate — superseded, kept as the record of why \
         fixtures were the bug; its header now says so",
    ),
];

/// Is this line a QUOTATION rather than an INSTRUCTION?
///
/// A markdown blockquote is literally "this is what something else said". A document
/// that corrects an old instruction has to be able to SHOW the old instruction — the
/// rewritten runbook quotes the verb it replaced so a reader arriving from a stale
/// bookmark recognises it, and forbidding that would force the correction to be
/// vaguer than the mistake.
///
/// Deliberately narrow: only a leading `>`. A guard that accepted "but it is in a
/// comment" would excuse most of the file.
fn is_quotation(line: &str) -> bool {
    line.trim_start().starts_with('>')
}

fn tracked_text_files() -> Vec<PathBuf> {
    let out = std::process::Command::new("git")
        .current_dir(repo_root())
        .args(["ls-files"])
        .output()
        .expect("git ls-files");
    String::from_utf8_lossy(&out.stdout)
        .lines()
        .map(|l| repo_root().join(l))
        .filter(|p| {
            matches!(
                p.extension().and_then(|e| e.to_str()),
                Some("md" | "rs" | "py" | "toml" | "ron" | "txt" | "html")
            ) || p.file_name().and_then(|f| f.to_str()) == Some(".gitignore")
        })
        .collect()
}

#[test]
fn no_document_names_a_crate_that_was_removed() {
    let excused: BTreeSet<&str> = HISTORY.iter().map(|(p, _)| *p).collect();
    let mut hits: Vec<String> = Vec::new();

    for path in tracked_text_files() {
        let rel = path.strip_prefix(repo_root()).unwrap().to_string_lossy().into_owned();
        if excused.contains(rel.as_str()) {
            continue;
        }
        // This test file names the ghosts in order to ban them.
        if rel.ends_with("no_ghost_paths_or_verbs.rs") {
            continue;
        }
        let Ok(text) = std::fs::read_to_string(&path) else { continue };
        for (i, line) in text.lines().enumerate() {
            if is_quotation(line) {
                continue;
            }
            for g in GHOSTS {
                if line.contains(g) {
                    hits.push(format!("{rel}:{}: {}", i + 1, line.trim()));
                }
            }
        }
    }
    assert!(
        hits.is_empty(),
        "these name a crate or verb that no longer exists — a reader following them \
         runs a command the binary does not have:\n  {}",
        hits.join("\n  ")
    );
}

#[test]
fn every_excused_history_file_still_exists() {
    // A stale allowlist entry excuses nothing and hides the next gap.
    for (p, why) in HISTORY {
        assert!(
            repo_root().join(p).is_file(),
            "`{p}` is on the history allowlist ({why}) and is not there — remove the entry"
        );
    }
}

#[test]
fn every_excused_history_file_actually_mentions_a_ghost() {
    // The other direction: an entry that no longer needs excusing is an exception
    // nobody can justify, and exceptions are what guards die of.
    for (p, _) in HISTORY {
        let text = std::fs::read_to_string(repo_root().join(p)).unwrap();
        assert!(
            GHOSTS.iter().any(|g| text.contains(g)),
            "`{p}` no longer mentions any removed crate — take it off the allowlist"
        );
    }
}

/// Every verb a dispatcher answers to, read from its `main.rs` rather than listed here.
/// For xtask that includes the verbs that moved to facett-ops: `cargo xtask publish-pip`
/// is answered BY NAME with its new door (`cargo ops publish-pip`), which is not the
/// dead end this guard exists for.
fn real_verbs_in(main_rs: &str) -> BTreeSet<String> {
    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();
        // `        "wrap-wasm" => wrap_wasm(),`
        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("publish-pip"), "the verb scan of {main_rs} found nothing real: {v:?}");
    v
}

fn real_verbs() -> BTreeSet<String> {
    real_verbs_in("xtask/src/main.rs")
}

#[test]
fn every_documented_xtask_verb_exists() {
    // THE general guard. `cargo xtask <verb>` appearing in a document is an
    // INSTRUCTION; if the dispatcher has no such arm the instruction is a dead end,
    // and the reader cannot tell that from their own mistake.
    let verbs = real_verbs();
    let mut dead: Vec<String> = Vec::new();

    for path in tracked_text_files() {
        let rel = path.strip_prefix(repo_root()).unwrap().to_string_lossy().into_owned();
        if rel == "xtask/src/main.rs" || rel.ends_with("no_ghost_paths_or_verbs.rs") {
            continue;
        }
        let Ok(text) = std::fs::read_to_string(&path) else { continue };
        for (i, line) in text.lines().enumerate() {
            if is_quotation(line) {
                continue;
            }
            let mut rest = line;
            while let Some(at) = rest.find("cargo xtask ") {
                rest = &rest[at + "cargo xtask ".len()..];
                let verb: String = rest
                    .chars()
                    .take_while(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
                    .collect();
                if verb.is_empty() || verb == "help" {
                    continue;
                }
                if !verbs.contains(&verb) {
                    dead.push(format!("{rel}:{}: `cargo xtask {verb}`", i + 1));
                }
            }
        }
    }
    assert!(
        dead.is_empty(),
        "these documents instruct the reader to run a verb the xtask does not have \
         (it answers to: {}):\n  {}",
        verbs.iter().cloned().collect::<Vec<_>>().join(", "),
        dead.join("\n  ")
    );
}

/// The same guard for the second door: every `cargo ops <verb>` a document instructs
/// must be a verb facett-ops has, and every verb xtask refuses as MOVED must be one —
/// a pointer to a door that does not answer is the dead end again.
#[test]
fn every_documented_ops_verb_exists_and_every_moved_verb_landed() {
    let ops = real_verbs_in("facett-ops/src/main.rs");
    let main = std::fs::read_to_string(repo_root().join("xtask/src/main.rs")).unwrap();
    let moved: Vec<String> = main
        .lines()
        .filter(|l| l.contains("=> moved("))
        .filter_map(|l| l.trim().strip_prefix('"')?.split_once('"').map(|(n, _)| n.to_string()))
        .collect();
    assert!(moved.len() >= 8, "the moved-verb scan found {moved:?}");
    for m in &moved {
        assert!(ops.contains(m), "xtask points `{m}` at `cargo ops {m}`, and facett-ops has no such verb");
    }
    let mut dead: Vec<String> = Vec::new();
    for path in tracked_text_files() {
        let rel = path.strip_prefix(repo_root()).unwrap().to_string_lossy().into_owned();
        if rel.ends_with("no_ghost_paths_or_verbs.rs") {
            continue;
        }
        let Ok(text) = std::fs::read_to_string(&path) else { continue };
        for (i, line) in text.lines().enumerate() {
            if is_quotation(line) {
                continue;
            }
            let mut rest = line;
            while let Some(at) = rest.find("cargo ops ") {
                rest = &rest[at + "cargo ops ".len()..];
                let verb: String =
                    rest.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect();
                if verb.is_empty() || verb == "help" || verb == "{}" {
                    continue;
                }
                if !ops.contains(&verb) {
                    dead.push(format!("{rel}:{}: `cargo ops {verb}`", i + 1));
                }
            }
        }
    }
    assert!(dead.is_empty(), "documents name `cargo ops` verbs facett-ops does not have:\n  {}", dead.join("\n  "));
}