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

nordisk/facett

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