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) => (¤t[..=i], ¤t[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(¤t);
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);
}