facett-core 0.1.13

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The functional-status → nornir test-matrix bridge** (feature `testmatrix`).
//!
//! This is the single, discoverable seam every facett crate's tests + self-test
//! / headless-render paths use to feed the `nornir test` matrix one row per
//! **real, meaningful check**. It wraps [`nornir_testmatrix::functional_status`]
//! so a leaf crate's test only has to call `facett_core::testmatrix::emit(...)`
//! (or [`emit_render`]) — it never has to depend on `nornir-testmatrix`
//! directly; this crate owns that edge.
//!
//! ## Gated so release strips it
//! Everything here is `#[cfg(feature = "testmatrix")]`. A facett crate adds a
//! passthrough `testmatrix = ["facett-core/testmatrix"]` and wraps each call in
//! `#[cfg(feature = "testmatrix")]`. So a **release build** (feature OFF):
//! * has no `nornir-testmatrix` dependency edge (it is `dep:` optional),
//! * compiles out every emit call site, and
//! * even a stray call would hit a no-op (the upstream function is itself a
//!   no-op when its own `testmatrix` feature is off).
//!
//! With `--features testmatrix` each call appends one functional row to the JSON
//! sink at `$NORNIR_TESTMATRIX_OUT` (default `target/nornir-functional.json`),
//! which `nornir test` reads back into the matrix. A blank / zero-geometry
//! render becomes a **RED** functional row (`ok = false`); a real render is green.

/// Emit one functional-status row for a single real check.
///
/// * `component` — the unit under test (a facett component / view name).
/// * `check` — the specific assertion (e.g. `"renders_non_blank"`).
/// * `ok` — the **real** assertion result (`true` → pass, `false` → red row).
/// * `detail` — the **measured** value (a count, a ratio, a reason …).
///
/// No-op (and zero-cost) when the `testmatrix` feature is off.
#[cfg(feature = "testmatrix")]
pub fn emit(component: &str, check: &str, ok: bool, detail: &str) {
    nornir_testmatrix::functional_status(component, check, ok, detail);
}

/// No-op shape compiled when `testmatrix` is OFF — the release path.
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit(_component: &str, _check: &str, _ok: bool, _detail: &str) {}

/// Convenience for a **headless render** check: a render is OK only when it drew
/// real geometry AND the component reports non-zero domain geometry
/// (`geometry > 0`). A blank framebuffer (`vertices == 0`) or a zero-geometry
/// component (`geometry == 0`, e.g. a dead map with no ways/points) emits a RED
/// row so a dead view is visible in the matrix.
///
/// `geometry` is the component's own count of what it should be drawing — ways +
/// points for a map, nodes for a graph, rows for a table — read from its
/// `state_json`. `vertices` is the harness's tessellated-mesh proof that the GPU
/// path actually produced primitives.
#[cfg(feature = "testmatrix")]
pub fn emit_render(component: &str, vertices: usize, geometry: usize) {
    let ok = vertices > 0 && geometry > 0;
    emit(
        component,
        "headless_render",
        ok,
        &format!("vertices={vertices} geometry={geometry}"),
    );
}

/// No-op shape compiled when `testmatrix` is OFF — the release path.
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit_render(_component: &str, _vertices: usize, _geometry: usize) {}

/// Convenience for a **pixel non-blank** render proof (the map / geomap class):
/// OK only when the measured content ratio clears `min_ratio` AND `geometry > 0`.
/// A blank pane (ratio ≈ 0) or zero-geometry view emits a RED row.
#[cfg(feature = "testmatrix")]
pub fn emit_non_blank(component: &str, ratio: f64, min_ratio: f64, geometry: usize) {
    let ok = ratio >= min_ratio && geometry > 0;
    emit(
        component,
        "renders_non_blank",
        ok,
        &format!("content_ratio={ratio:.4} min={min_ratio} geometry={geometry}"),
    );
}

/// No-op shape compiled when `testmatrix` is OFF — the release path.
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit_non_blank(_component: &str, _ratio: f64, _min_ratio: f64, _geometry: usize) {}

// ── The discovery-driven functional cell (backfills the zero-cell panes) ─────
//
// [`emit_facet_probe`] turns one [`FacetProbe`](crate::harness::FacetProbe) — the
// output of headless-rendering + JSON-driving ANY `dyn Facet` — into the set of
// functional rows a pane needs, in ONE call. This is the shared path the ~14
// panes that emit no matrix cell adopt: a leaf's test builds its pane via its own
// `local()` demo constructor, calls
// [`probe_facet`](crate::harness::probe_facet), and hands the result here — no
// per-crate render+emit boilerplate. Everything is `#[cfg(feature = "testmatrix")]`
// so release strips the edge, exactly like [`emit`].

/// Emit the functional rows a probed pane needs from one
/// [`FacetProbe`](crate::harness::FacetProbe):
/// * `renders_non_blank` — the initial headless render produced geometry AND the
///   pane reports non-empty domain state (a blank OR zero-cardinality pane → RED);
/// * `responds_to_input` — at least one scripted `update_json` message moved
///   `state_json` (only emitted when messages were scripted; a dead input surface
///   → RED).
///
/// No-op (zero-cost) when the `testmatrix` feature is off.
#[cfg(feature = "testmatrix")]
pub fn emit_facet_probe(component: &str, probe: &crate::harness::FacetProbe) {
    emit_render(component, probe.vertices, probe.cardinality());
    if !probe.steps.is_empty() {
        emit(
            component,
            "responds_to_input",
            probe.responded(),
            &format!(
                "changed={}/{} steps",
                probe.steps.iter().filter(|s| s.changed).count(),
                probe.steps.len()
            ),
        );
    }
}

/// No-op shape compiled when `testmatrix` is OFF — the release path.
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit_facet_probe(_component: &str, _probe: &crate::harness::FacetProbe) {}

/// One-shot convenience: [`probe_facet`](crate::harness::probe_facet) the pane
/// through `msgs`, then [`emit_facet_probe`] the result. The single line a leaf's
/// discovery test writes to give its pane a functional matrix cell. Returns the
/// probe so the test can add pane-specific assertions on top.
pub fn probe_and_emit(
    component: &str,
    facet: &mut dyn crate::Facet,
    msgs: &[&str],
) -> crate::harness::FacetProbe {
    let probe = crate::harness::probe_facet(facet, msgs);
    emit_facet_probe(component, &probe);
    probe
}

#[cfg(all(test, feature = "testmatrix"))]
mod tests {
    use super::*;

    /// The recorder reads `NORNIR_TESTMATRIX_OUT` from the process-global env at
    /// each `record` call, so the two tests that set/clear it must NOT run
    /// concurrently (cargo runs tests in parallel) or one clears the other's sink
    /// mid-emit. Serialize them behind this lock.
    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

    /// INJECT-AND-ASSERT: a real render report → a real functional row on the
    /// sink. We point the sink at a temp file, emit a green + a red render, and
    /// assert both landed with the right status (the matrix's RED/green contract).
    #[test]
    fn emit_render_writes_green_and_red_rows() {
        let _env = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
        let dir = std::env::temp_dir().join(format!(
            "facett-tm-{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let file = dir.join("functional.json");
        // SAFETY (edition 2024): the ENV_LOCK above serializes the env-touching
        // run single-threaded for the duration of the assertions; no other
        // thread reads the environment concurrently here.
        unsafe {
            std::env::set_var("NORNIR_TESTMATRIX_OUT", &file);
            std::env::set_var("NORNIR_TESTMATRIX_REPO", "facett");
            std::env::set_var("NORNIR_TESTMATRIX_RUN", "run-fc-test");
        }

        // A live view: drew 1234 verts over 9 geometry → green.
        emit_render("live_view", 1234, 9);
        // A dead map: 0 geometry even though something drew → RED.
        emit_render("dead_map", 50, 0);
        // A blank pane: ratio under floor → RED.
        emit_non_blank("blank_pane", 0.0007, 0.01, 40_000);
        // A real pane: ratio over floor + geometry → green.
        emit_non_blank("real_map", 0.028, 0.01, 40_000);

        let rows = nornir_testmatrix::JsonFileSink::new(&file)
            .read_all()
            .unwrap();
        assert_eq!(rows.len(), 4, "four checks → four functional rows");

        let live = rows.iter().find(|r| r.suite == "live_view").unwrap();
        assert_eq!(live.status, nornir_testmatrix::status::PASS);
        assert_eq!(live.aspect, "functional"); // functional_status sets aspect="functional" (no ASPECT_FUNCTIONAL const)

        let dead = rows.iter().find(|r| r.suite == "dead_map").unwrap();
        assert_eq!(dead.status, nornir_testmatrix::status::FAIL, "zero geometry = RED");

        let blank = rows.iter().find(|r| r.suite == "blank_pane").unwrap();
        assert_eq!(blank.status, nornir_testmatrix::status::FAIL, "blank pane = RED");

        let real = rows.iter().find(|r| r.suite == "real_map").unwrap();
        assert_eq!(real.status, nornir_testmatrix::status::PASS);

        unsafe {
            std::env::remove_var("NORNIR_TESTMATRIX_OUT");
            std::env::remove_var("NORNIR_TESTMATRIX_REPO");
            std::env::remove_var("NORNIR_TESTMATRIX_RUN");
        }
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// A pane with a live `update_json` surface, for the probe→emit test.
    struct ProbeListPane {
        items: Vec<String>,
    }
    impl crate::Facet for ProbeListPane {
        fn title(&self) -> &str {
            "probe_list"
        }
        fn ui(&mut self, ui: &mut egui::Ui) {
            for it in &self.items {
                ui.label(it);
            }
        }
        fn state_json(&self) -> serde_json::Value {
            serde_json::json!({ "items": self.items })
        }
        fn update_json(&mut self, msg_json: &str) {
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json)
                && let Some(s) = v.get("push").and_then(|x| x.as_str())
            {
                self.items.push(s.to_string());
            }
        }
    }

    /// INJECT-AND-ASSERT: driving a real pane through `probe_and_emit` writes the
    /// discovery-driven functional rows — a green render row (drew + non-empty
    /// state) and a green `responds_to_input` row — onto the sink.
    #[test]
    fn probe_and_emit_writes_discovery_rows() {
        let _env = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
        let dir = std::env::temp_dir().join(format!(
            "facett-tm-probe-{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let file = dir.join("functional.json");
        unsafe {
            std::env::set_var("NORNIR_TESTMATRIX_OUT", &file);
            std::env::set_var("NORNIR_TESTMATRIX_REPO", "facett");
            std::env::set_var("NORNIR_TESTMATRIX_RUN", "run-probe");
        }

        let mut pane = ProbeListPane { items: vec!["seed".into()] };
        let probe = probe_and_emit("probe_list", &mut pane, &[r#"{"push":"a"}"#, r#"{"push":"b"}"#]);
        assert!(probe.responded(), "both pushes move state");
        assert_eq!(pane.items.len(), 3);

        let rows = nornir_testmatrix::JsonFileSink::new(&file).read_all().unwrap();
        // One render row + one responds_to_input row.
        assert_eq!(rows.len(), 2, "probe emits render + input rows");

        let render = rows.iter().find(|r| r.test_name == "headless_render").unwrap();
        assert_eq!(render.suite, "probe_list");
        assert_eq!(render.status, nornir_testmatrix::status::PASS, "drew + non-empty state = green");

        let input = rows.iter().find(|r| r.test_name == "responds_to_input").unwrap();
        assert_eq!(input.status, nornir_testmatrix::status::PASS, "live update_json = green");

        unsafe {
            std::env::remove_var("NORNIR_TESTMATRIX_OUT");
            std::env::remove_var("NORNIR_TESTMATRIX_REPO");
            std::env::remove_var("NORNIR_TESTMATRIX_RUN");
        }
        let _ = std::fs::remove_dir_all(&dir);
    }
}