facett-core/tests/errcode_raise_sites.rs
//! **The RAISE-SITE guard + the empty-vs-unreadable twin table.**
//!
//! facett's catalog guards (`errcode::tests`, `registry_md_fresh.rs`) prove the REGISTRY
//! is internally consistent and that the exported markdown / Python JSON match it. None
//! of them looks at the **raise sites**, and there is a hole there:
//!
//! ```ignore
//! macro_rules! facet_err {
//! ($comp:ident, $n:literal, $($msg:tt)*) => {
//! ::core::concat!("facet-", stringify!($comp), "-", stringify!($n)), // ← never consulted
//! ```
//!
//! `facet_err!` **fabricates** its code with `concat!` and never asks the registry
//! whether that code exists. `facet_err!(map, 99, …)` compiles perfectly and produces an
//! authoritative-looking `facet-map-99` that `lookup` cannot resolve, `reaction_for`
//! answers `None` for (so the consumer has NO defined reaction), and `source_url`
//! silently degrades to the repo root.
//!
//! The second half of this binary is the reason the codes exist at all. From today's
//! ⚒ Build Thing post-mortem: a bug recurred FOUR times because the UI could not
//! distinguish *"there is no data"* from *"I could not read the store."* Two failures
//! that render identically make a bug immortal. Several components declared an
//! `EmptyState` code with **no companion** for the read failure, so both worlds landed
//! on the same honest-looking empty pane. The twin table below pins each pair and — the
//! load-bearing assertion — requires the two to carry **different reactions**, because
//! reacting identically is exactly the defect.
//!
//! ```bash
//! cargo test -p facett-core --test errcode_raise_sites
//! ```
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use facett_core::errcode::{self, Reaction};
/// One `facet_err!(component, n, …)` occurrence found in facett's source.
#[derive(Debug, Clone)]
struct RaiseSite {
/// Workspace-relative path, e.g. `facett-demo/src/errcode_tab.rs`.
file: String,
line: usize,
component: String,
n: u32,
}
impl RaiseSite {
fn code(&self) -> String {
format!("facet-{}-{}", self.component, self.n)
}
}
/// The facett workspace root (parent of `facett-core`).
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("facett-core sits in the facett workspace")
.to_path_buf()
}
/// Every `*.rs` under any member crate's `src/`.
fn rust_sources() -> Vec<PathBuf> {
let root = workspace_root();
let mut out = Vec::new();
let Ok(members) = std::fs::read_dir(&root) else { return out };
for member in members.flatten() {
let src = member.path().join("src");
if !src.is_dir() {
continue;
}
let mut stack = vec![src];
while let Some(dir) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&dir) else { continue };
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
}
out.sort();
out
}
/// Blank comment lines (preserving line numbers) and remove `#[cfg(test)]` items.
///
/// **This function had a bug worth remembering.** It used to `break` at the first
/// top-level `#[cfg(test)]`, assuming test modules always sit at the END of a file. They
/// do not: `crates/dwarves/src/vault.rs` has one at line 78 of 1100, so the scanner threw
/// away every real raise site after it and the guard passed while seeing almost nothing —
/// the exact "a broken scanner makes this guard vacuous" failure the count assertion
/// exists to catch, and it caught it.
///
/// Now it skips only the attributed ITEM, by matching braces from that item's opening `{`
/// to its close, and keeps the rest of the file.
fn production_source(text: &str) -> String {
let lines: Vec<&str> = text.lines().collect();
let mut out = String::with_capacity(text.len());
let mut i = 0usize;
while i < lines.len() {
let line = lines[i];
let t = line.trim_start();
if t.starts_with("//") {
out.push('\n'); // keep the line count, drop the content
i += 1;
continue;
}
if t.starts_with("#[cfg(test)]") || t.starts_with("#[cfg(all(test") {
// Skip forward to this item's opening brace, then to its matching close.
let mut depth = 0i32;
let mut opened = false;
while i < lines.len() {
for c in lines[i].chars() {
match c {
'{' => {
depth += 1;
opened = true;
}
'}' => depth -= 1,
_ => {}
}
}
out.push('\n'); // preserve line numbering for the skipped item
i += 1;
if opened && depth <= 0 {
break;
}
// A `#[cfg(test)] use …;` has no braces at all — stop at its semicolon.
if !opened && lines[i - 1].trim_end().ends_with(';') {
break;
}
}
continue;
}
out.push_str(line);
out.push('\n');
i += 1;
}
out
}
/// Scan facett's source for `facet_err!(<ident>, <int>,` occurrences — whitespace and
/// newline tolerant, so a multi-line raise is never silently missed.
fn macro_raise_sites() -> Vec<RaiseSite> {
const NEEDLE: &str = "facet_err!(";
let root = workspace_root();
let mut out = Vec::new();
for path in rust_sources() {
let Ok(raw) = std::fs::read_to_string(&path) else { continue };
let text = production_source(&raw);
let rel = path
.strip_prefix(&root)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
let mut cursor = 0usize;
while let Some(rel_pos) = text[cursor..].find(NEEDLE) {
let pos = cursor + rel_pos;
let args_start = pos + NEEDLE.len();
cursor = args_start;
let after = &text[args_start..];
let Some((comp_raw, tail)) = after.split_once(',') else { continue };
let Some((n_raw, _)) = tail.split_once(',') else { continue };
let component = comp_raw.trim();
let n_str = n_raw.trim();
// A macro DEFINITION arm (`$comp:ident`) is not a raise site.
if component.starts_with('$') || component.is_empty() {
continue;
}
let Ok(n) = n_str.parse::<u32>() else { continue };
out.push(RaiseSite {
file: rel.clone(),
line: text[..pos].matches('\n').count() + 1,
component: component.to_string(),
n,
});
}
}
out
}
/// Scan for the OTHER way a code reaches a user: `.error_code("facet-<comp>-<n>")` on an
/// a11y node.
///
/// **Why this exists.** The scanner above matched only `facet_err!(`, and the entire
/// workspace contains exactly TWO such calls — both in `facett-demo`'s showcase tab. Every
/// code a real pane actually raises reaches the accessibility tree through
/// `.error_code("…")` instead, and there were **12** of those, in `facett-cfd`,
/// `facett-geomap`, `facett-graph3d`, `facett-graphview`, `facett-korp`, `facett-map`,
/// `facett-map3d`, `facett-syschart` and `facett-demo`. The guard was policing 2 sites in a
/// demo while the 12 that ship went unchecked.
///
/// That is the failure mode this whole error-code scheme exists to kill, one level up: a
/// check that reports green over a population it cannot see. A guard is only as wide as
/// its needle.
fn a11y_raise_sites() -> Vec<RaiseSite> {
const NEEDLE: &str = ".error_code(\"facet-";
let root = workspace_root();
let mut out = Vec::new();
for path in rust_sources() {
let Ok(raw) = std::fs::read_to_string(&path) else { continue };
let text = production_source(&raw);
let rel = path
.strip_prefix(&root)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
let mut cursor = 0usize;
while let Some(rel_pos) = text[cursor..].find(NEEDLE) {
let pos = cursor + rel_pos;
let body_start = pos + NEEDLE.len();
cursor = body_start;
// `<component>-<n>"` — component may contain '-', so split at the LAST '-'.
let Some(end) = text[body_start..].find('"') else { continue };
let body = &text[body_start..body_start + end];
let Some((component, n_str)) = body.rsplit_once('-') else { continue };
let Ok(n) = n_str.trim().parse::<u32>() else { continue };
if component.is_empty() {
continue;
}
out.push(RaiseSite {
file: rel.clone(),
line: text[..pos].matches('\n').count() + 1,
component: component.to_string(),
n,
});
}
}
out
}
/// Every way a code reaches a user. Both channels, always — the scheme's own doctrine.
fn raise_sites() -> Vec<RaiseSite> {
let mut out = macro_raise_sites();
out.extend(a11y_raise_sites());
out
}
/// A guard that matches nothing passes forever. facett raises few codes today (most of
/// the catalog is declarative, awaiting pane wiring), but the showcase raises real ones
/// and the scanner must see them.
#[test]
fn the_scanner_finds_the_raise_sites() {
let sites = raise_sites();
eprintln!(
"[raise-sites] {} facet_err! call sites across the workspace: {:?}",
sites.len(),
sites.iter().map(RaiseSite::code).collect::<Vec<_>>()
);
assert!(
!sites.is_empty(),
"the scanner found NO facet_err! raise sites — it is broken, and a broken scanner \
makes this whole guard vacuous"
);
assert!(
sites.iter().any(|s| s.component == "demo"),
"the scanner must see the showcase's demo raises in facett-demo/src/errcode_tab.rs"
);
// The scanner must see BOTH channels. Before this assertion existed the guard matched
// only `facet_err!(` — 2 sites, both in the demo — while the 12 `.error_code("…")`
// sites that real panes actually ship went entirely unchecked.
let a11y = a11y_raise_sites();
assert!(
!a11y.is_empty(),
"the scanner found NO `.error_code(\"facet-…\")` sites. Real panes raise codes into \
the a11y tree, NOT through facet_err! — if this half is blind the guard is policing \
a showcase and nothing that ships"
);
// Named crates, so a regression that narrows the sweep is caught by name and not by a
// count that could be satisfied by any two files.
for crate_dir in ["facett-korp", "facett-map", "facett-graphview"] {
assert!(
a11y.iter().any(|s| s.file.starts_with(crate_dir)),
"no a11y raise site seen in {crate_dir} — the scanner's reach has narrowed"
);
}
}
/// **THE GUARD: no error path may raise an UNDOCUMENTED code.**
///
/// Red-proved: `facet_err!(map, 99, …)` in any member crate's `src/` fails this with
/// `facet-map-99 … not in the REGISTRY`.
#[test]
fn every_raise_site_names_a_documented_code() {
let mut undocumented = Vec::new();
for s in raise_sites() {
if errcode::lookup_cn(&s.component, s.n).is_none() {
undocumented.push(format!(
"{}:{} raises {} — not in the REGISTRY",
s.file,
s.line,
s.code()
));
}
}
assert!(
undocumented.is_empty(),
"{} error path(s) raise a code the catalog does not document. `facet_err!` builds \
its code with `concat!` and never checks the registry, so these COMPILE, and a \
consumer then gets a code with NO defined reaction:\n {}",
undocumented.len(),
undocumented.join("\n "),
);
}
/// **Catalog drift: the file a row advertises must really raise that code.**
///
/// A row publishes `crate_dir` + `src_file`, which become the source link an operator
/// clicks. One condition may be raised from several places, so the invariant is "the
/// declared file is AMONG the raise sites". Codes with no raise site at all are out of
/// scope — most of facett's catalog is still declarative, awaiting pane wiring.
#[test]
fn the_file_each_row_advertises_really_raises_that_code() {
let mut by_code: HashMap<String, Vec<String>> = HashMap::new();
for s in raise_sites() {
by_code.entry(s.code()).or_default().push(s.file.clone());
}
let mut drift = Vec::new();
for (code, files) in &by_code {
let Some(row) = errcode::lookup(code) else { continue };
let declared = format!("{}/{}", row.crate_dir, row.src_file);
if !files.iter().any(|f| f == &declared) {
let mut seen = files.clone();
seen.sort();
seen.dedup();
drift.push(format!("{code} declares {declared} but is raised only in {seen:?}"));
}
}
drift.sort();
assert!(
drift.is_empty(),
"{} code(s) advertise a source file that does not raise them:\n {}",
drift.len(),
drift.join("\n "),
);
}
/// **No `(component, n)` pair is documented twice.**
#[test]
fn no_component_n_pair_is_documented_twice() {
let mut seen: HashMap<(&str, u32), usize> = HashMap::new();
for e in errcode::registry() {
*seen.entry((e.component, e.n)).or_insert(0) += 1;
}
let dupes: Vec<String> = seen
.iter()
.filter(|&(_, &c)| c > 1)
.map(|((comp, n), c)| format!("facet-{comp}-{n} declared {c}×"))
.collect();
assert!(dupes.is_empty(), "DUPLICATE registry rows: {dupes:?}");
}
// ── the empty-vs-unreadable twin table ───────────────────────────────────────
/// Each row: **(the "there is no data" code, the "I could not read it" code, what the
/// pair used to look like)**. These are the pairs that rendered identically before the
/// companions existed.
const TWINS: &[(&str, &str, &str)] = &[
("facet-map-2", "facet-map-4", "no vessels to plot ⇄ the vessel source could not be read"),
("facet-map3d-1", "facet-map3d-4", "nothing to extrude ⇄ the ways/buildings source could not be read"),
("facet-geomap-2", "facet-geomap-1", "no points in view ⇄ the GeoParquet source did not load"),
("facet-graph-1", "facet-graph-2", "empty scene ⇄ the scene source could not be read"),
("facet-graph3d-1", "facet-graph3d-4", "no nodes to render ⇄ the node source could not be read"),
("facet-graphview-1", "facet-graphview-3", "empty scene ⇄ the graph source did not load"),
("facet-population-1", "facet-population-2", "no people at this filter ⇄ the model could not build the sample"),
("facet-systemmap-1", "facet-systemmap-3", "nothing to draw ⇄ the topology source could not be read"),
("facet-syschart-1", "facet-syschart-3", "no peers ⇄ the peer roster could not be read"),
("facet-cfd-2", "facet-cfd-1", "the pipe network is empty ⇄ the solver produced a non-finite cell"),
("facet-korp-2", "facet-korp-3", "no cases to display ⇄ the case query FAILED on a live connection"),
];
/// **THE POINT OF THE WHOLE PASS.** For every pair: both codes exist, the first really
/// is the `EmptyState` ("there is no data") code, and — the load-bearing bit — the two
/// carry **DIFFERENT reactions**. A consumer that reacts to the code therefore cannot
/// treat "nothing here" and "I could not read it" the same way, which is precisely how
/// the ⚒ Build Thing bug survived four fixes.
#[test]
fn every_empty_state_has_a_distinguishable_unreadable_twin() {
for &(empty, unreadable, what) in TWINS {
let e = errcode::lookup(empty)
.unwrap_or_else(|| panic!("{empty} must be registered ({what})"));
let u = errcode::lookup(unreadable)
.unwrap_or_else(|| panic!("{unreadable} must be registered ({what})"));
assert_eq!(
e.reaction,
Reaction::EmptyState,
"{empty} is the 'there is no data' half of [{what}] and must react EmptyState"
);
assert_ne!(
u.reaction,
Reaction::EmptyState,
"{unreadable} is the 'I could not read it' half of [{what}] — it must NOT \
react EmptyState, or the two are indistinguishable again"
);
assert_ne!(
e.reaction, u.reaction,
"[{what}] — {empty} and {unreadable} react identically ({:?}); two different \
failures with one reaction is the bug this table exists to prevent",
e.reaction
);
}
eprintln!("[twins] {} empty-vs-unreadable pairs, all distinguishable", TWINS.len());
}
/// **Every `EmptyState` code is covered by the twin table.** Without this, adding a new
/// empty-state code with no companion would silently reintroduce the ambiguity — the
/// table would just not mention it.
#[test]
fn no_empty_state_code_is_missing_from_the_twin_table() {
let covered: HashSet<&str> = TWINS.iter().map(|&(empty, _, _)| empty).collect();
let uncovered: Vec<&str> = errcode::registry()
.iter()
.filter(|e| e.reaction == Reaction::EmptyState)
.map(|e| e.code)
.filter(|c| !covered.contains(c))
.collect();
assert!(
uncovered.is_empty(),
"these EmptyState code(s) have no 'could not read' twin declared in TWINS: {uncovered:?} \
— an empty pane that cannot say WHY it is empty is the ⚒ Build Thing bug. Add the \
companion code to the REGISTRY and pair it here."
);
}
/// **The connection failure split.** `facet-korp-1` used to be the only thing a dead
/// backend could say, lumping REFUSED, TIMED-OUT and REJECTED together. They need three
/// different repairs, so they must carry three different reactions — in particular a
/// rejected identity must NOT tell the UI to keep reconnecting.
#[test]
fn refused_timed_out_and_rejected_are_three_different_reactions() {
let refused = errcode::lookup("facet-korp-4").expect("facet-korp-4 registered");
let timeout = errcode::lookup("facet-korp-5").expect("facet-korp-5 registered");
let denied = errcode::lookup("facet-korp-6").expect("facet-korp-6 registered");
assert_eq!(refused.reaction, Reaction::Reconnect, "nothing listening ⇒ reconnect");
assert_eq!(timeout.reaction, Reaction::Retry, "listening but slow ⇒ retry");
assert_eq!(
denied.reaction,
Reaction::Diagnose,
"a REJECTED identity must not be retried or reconnected forever — it needs a human"
);
let reactions: HashSet<Reaction> =
[refused.reaction, timeout.reaction, denied.reaction].into_iter().collect();
assert_eq!(
reactions.len(),
3,
"refused / timed-out / rejected must be three DISTINCT reactions, not one"
);
}