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

nordisk/holger

errcode/tests/registry_doc_is_current.rs

//! **The registry doc is the registry** (HOL-DOCFIND d): the tables of
//! `.nornir/holger-error-codes.md` are GENERATED from [`holger_errcode::ALL`], and this guard
//! fails the moment the file and the registry disagree (a code added, a summary reworded, a
//! row moved from `reserved` to `wired`). Before it, the doc was written by hand and said
//! "116 codes" while the registry held 332.
//!
//! What a person writes stays a person's: the text above `## The registry`, each subsystem's
//! heading line and the prose under it are kept as they are; the headline counts, each
//! subsystem's count line, its table and its next free ordinal are the registry's.
//!
//! Regenerate after a registry change:
//!
//! ```text
//! HOLGER_ERRCODES_REGEN=1 cargo test -p holger-errcode --test registry_doc_is_current
//! ```

use holger_errcode::{ErrCode, Wiring, ALL};
use std::collections::BTreeMap;
use std::path::PathBuf;

const DOC: &str = ".nornir/holger-error-codes.md";
const REGISTRY_HEADING: &str = "## The registry";

fn doc_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("errcode/.. is the holger repo root")
        .join(DOC)
}

/// The subsystem a heading line names: ``### `sub` — …`` (or `##`), else `None`.
fn heading_subsystem(line: &str) -> Option<String> {
    let rest = line
        .strip_prefix("### `")
        .or_else(|| line.strip_prefix("## `"))?;
    let (sub, _) = rest.split_once('`')?;
    Some(sub.to_owned())
}

fn is_count_line(line: &str) -> Option<&str> {
    // `N code(s) — **W wired**, **R reserved**.` and whatever a person wrote after it.
    let (n, rest) = line.split_once(' ')?;
    n.parse::<usize>().ok()?;
    let rest = rest.strip_prefix("code(s) — ")?;
    let (_, after) = rest.split_once("reserved**.")?;
    Some(after.trim())
}

/// The prose of one old section: everything but the generated parts.
fn prose(lines: &[&str]) -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    for l in lines {
        if l.starts_with('|') || l.starts_with("Next free ordinal") {
            continue;
        }
        if let Some(after) = is_count_line(l) {
            if !after.is_empty() {
                out.push(after.to_owned());
                out.push(String::new());
            }
            continue;
        }
        if l.trim().is_empty() && out.last().is_none_or(|p| p.trim().is_empty()) {
            continue;
        }
        out.push((*l).to_owned());
    }
    while out.last().is_some_and(|l| l.trim().is_empty()) {
        out.pop();
    }
    out
}

/// A new subsystem's heading, from the registry's own section comment
/// (`// ── <sub> — <title> ──` in `errcode/src/lib.rs`); `### `sub`` when it has none.
fn new_heading(sub: &str) -> String {
    let src = std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/lib.rs"))
        .unwrap_or_default();
    let title = src.lines().find_map(|l| {
        let rest = l.trim().strip_prefix("// ── ")?.strip_prefix(sub)?;
        let rest = rest
            .strip_prefix(" — ")
            .or_else(|| rest.strip_prefix(':'))
            .or_else(|| rest.strip_prefix(' '))?;
        let t = rest.trim_end_matches(['─', ' ', ':']).trim();
        (!t.is_empty()).then(|| t.to_owned())
    });
    match title {
        Some(t) => format!("### `{sub}` — {t}"),
        None => format!("### `{sub}`"),
    }
}

fn cell(s: &str) -> String {
    s.replace('|', "\\|")
}

fn counts(codes: &[&ErrCode]) -> (usize, usize) {
    let wired = codes.iter().filter(|c| c.wiring == Wiring::Wired).count();
    (wired, codes.len() - wired)
}

/// The doc as the registry says it, from the doc as it stands.
fn render(current: &str) -> String {
    let (head, old_registry) = match current.find(&format!("\n{REGISTRY_HEADING}")) {
        Some(i) => (&current[..=i], &current[i + 1..]),
        None => (current, ""),
    };
    // The old registry: its intro, and per subsystem its heading line and prose.
    let mut intro: Vec<&str> = Vec::new();
    let mut sections: BTreeMap<String, (String, Vec<&str>)> = BTreeMap::new();
    let mut at: Option<String> = None;
    for line in old_registry.lines().skip(1) {
        if let Some(sub) = heading_subsystem(line) {
            sections.insert(
                sub.clone(),
                (
                    line.replacen("## `", "### `", 1)
                        .replacen("#### `", "### `", 1),
                    Vec::new(),
                ),
            );
            at = Some(sub);
            continue;
        }
        match &at {
            Some(sub) => sections.get_mut(sub).expect("the section").1.push(line),
            None => intro.push(line),
        }
    }

    let mut order: Vec<&str> = Vec::new();
    let mut by_sub: BTreeMap<&str, Vec<&ErrCode>> = BTreeMap::new();
    for c in ALL {
        if !by_sub.contains_key(c.subsystem) {
            order.push(c.subsystem);
        }
        by_sub.entry(c.subsystem).or_default().push(c);
    }
    let all: Vec<&ErrCode> = ALL.iter().collect();
    let (wired, reserved) = counts(&all);

    let mut out = String::from(head);
    out.push_str(&format!(
        "{REGISTRY_HEADING} — {} codes across {} subsystems, **{wired} wired / {reserved} reserved**\n\n",
        ALL.len(),
        order.len()
    ));
    let intro = prose(&intro);
    if !intro.is_empty() {
        out.push_str(&intro.join("\n"));
        out.push_str("\n\n");
    }
    for sub in order {
        let codes = &by_sub[sub];
        let (heading, old) = sections
            .get(sub)
            .map(|(h, l)| (h.clone(), prose(l)))
            .unwrap_or_else(|| (new_heading(sub), Vec::new()));
        out.push_str(&heading);
        out.push_str("\n\n");
        if !old.is_empty() {
            out.push_str(&old.join("\n"));
            out.push_str("\n\n");
        }
        let (w, r) = counts(codes);
        out.push_str(&format!(
            "{} code(s) — **{w} wired**, **{r} reserved**.\n\n",
            codes.len()
        ));
        out.push_str("| code | const | status | means | src |\n|---|---|---|---|---|\n");
        for c in codes {
            out.push_str(&format!(
                "| `{}` | `{}` | `{}` | {} | `{}` |\n",
                c.code,
                c.name().expect("every code in ALL has a name"),
                c.wiring.label(),
                cell(c.summary),
                c.src_file
            ));
        }
        let next = codes.iter().map(|c| c.n).max().unwrap_or(0) + 1;
        out.push_str(&format!("\nNext free ordinal in `{sub}`: **{next}**\n\n"));
    }
    while out.ends_with("\n\n") {
        out.pop();
    }
    out
}

#[test]
fn the_registry_doc_is_generated_from_the_registry() {
    let path = doc_path();
    let current =
        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
    let want = render(&current);
    if std::env::var_os("HOLGER_ERRCODES_REGEN").is_some() {
        std::fs::write(&path, &want).unwrap();
        return;
    }
    if current != want {
        let first = current
            .lines()
            .zip(want.lines())
            .position(|(a, b)| a != b)
            .unwrap_or_else(|| current.lines().count().min(want.lines().count()));
        panic!(
            "{DOC} is not the registry ({} codes); first difference at line {}:\n  doc:      {:?}\n  registry: {:?}\n\
             regenerate: HOLGER_ERRCODES_REGEN=1 cargo test -p holger-errcode --test registry_doc_is_current",
            ALL.len(),
            first + 1,
            current.lines().nth(first).unwrap_or(""),
            want.lines().nth(first).unwrap_or(""),
        );
    }
}

/// The generator keeps a person's prose and is a fixed point: rendering its own output
/// changes nothing.
#[test]
fn the_generator_keeps_prose_and_is_a_fixed_point() {
    let doc = "# head\n\nwords\n\n## The registry — 1 codes\n\nintro words\n\n### `update` — the gateway\n\nwhy it is\n\n3 code(s) — **1 wired**, **2 reserved**. Extra words.\n\n| code |\n|---|\n\nNext free ordinal in `update`: **2**\n";
    let once = render(doc);
    assert!(
        once.starts_with("# head\n\nwords\n\n## The registry — "),
        "{once}"
    );
    assert!(once.contains("\nintro words\n"), "{once}");
    assert!(
        once.contains("### `update` — the gateway\n\nwhy it is\n\nExtra words.\n\n"),
        "{once}"
    );
    assert!(
        once.contains("| `holger-update-1` | `UPDATE_SCHEMA_UNSUPPORTED` | `wired` |"),
        "{once}"
    );
    assert_eq!(render(&once), once);
}