facett-core 0.1.10

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **In-memory render/exec trace** — the wasm-safe "what actually RAN" ledger.
//!
//! Unlike [`crate::trace`] (a JSONL file at `$FACETT_TRACE`, native-only — there is
//! no filesystem in a browser), this is a process-global, **in-memory** counter
//! map: every component's `ui()`/render and every interactive control handler calls
//! [`ran`] with a stable key, and the running tally is read back as a JSON object
//! with [`snapshot`]. It is folded into the demo's `state_json["trace"]["ran"]`, so
//! the SAME mechanism proves execution natively (a headless egui_kittest drive) AND
//! in the shipped wasm bundle (read via the `window.__facett_state()` JS hook).
//!
//! This is the mechanism that was missing: a way to verify the SHIPPED wasm artifact
//! — that each tab/control actually ran in the browser — as readable data, with no
//! human and no filesystem.
//!
//! Cheap + dependency-free: a `Mutex<BTreeMap<String,u64>>` behind a `OnceLock`,
//! incremented on edge-triggered control handlers + once per active-facet render.
//! `BTreeMap` keeps the snapshot key order stable (deterministic for tests).

use std::collections::BTreeMap;
use std::sync::{Mutex, OnceLock};

static LEDGER: OnceLock<Mutex<BTreeMap<String, u64>>> = OnceLock::new();

fn ledger() -> &'static Mutex<BTreeMap<String, u64>> {
    LEDGER.get_or_init(|| Mutex::new(BTreeMap::new()))
}

/// Record that the component/control identified by `key` RAN once (e.g.
/// `"PopGraphTab::ui"`, `"PopGraphTab::ctl:11M"`). Increments its tally. Never
/// panics (a poisoned lock is silently ignored — a trace must never crash the UI).
pub fn ran(key: &str) {
    if let Ok(mut g) = ledger().lock() {
        *g.entry(key.to_string()).or_insert(0) += 1;
    }
}

/// The current tally as a JSON object `{ "<key>": <count>, … }` — the readable
/// "what ran" list. Folded into the demo's `state_json` so a headless drive (native)
/// or the `window.__facett_state()` JS hook (wasm) reads exactly what executed.
pub fn snapshot() -> serde_json::Value {
    let g = match ledger().lock() {
        Ok(g) => g,
        Err(_) => return serde_json::json!({}),
    };
    let map: serde_json::Map<String, serde_json::Value> =
        g.iter().map(|(k, v)| (k.clone(), serde_json::json!(v))).collect();
    serde_json::Value::Object(map)
}

/// How many distinct keys have ever run (the breadth of the trace).
pub fn distinct() -> usize {
    ledger().lock().map(|g| g.len()).unwrap_or(0)
}

/// The count recorded for one key (0 if it never ran). The inject-assert seam: a
/// test reads back whether a specific control's handler actually executed.
pub fn count(key: &str) -> u64 {
    ledger().lock().map(|g| g.get(key).copied().unwrap_or(0)).unwrap_or(0)
}

/// Clear the ledger — for test isolation (each test starts from a clean slate so a
/// previous test's runs don't leak into its assertions). The global is shared, so a
/// test that asserts counts should `reset()` first.
pub fn reset() {
    if let Ok(mut g) = ledger().lock() {
        g.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The ledger is a process-global, so these tests — which `reset()` it and then
    /// assert counts — must not run concurrently with *each other*: one's `reset()`
    /// would wipe the other's keys mid-assertion. Serialize them here. (They stay
    /// robust against *other* parallel tests in this binary — e.g. `FacetDeck`
    /// renders that call `ran("deck.render:…")` — because every assertion below is
    /// scoped to this test's own keys, never the global `distinct()` total.)
    static LEDGER_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());

    /// INJECT-ASSERT: recording real keys produces a real, readable tally — the
    /// snapshot reflects exactly what ran, with per-key counts, not "didn't panic".
    #[test]
    fn records_and_snapshots_real_counts() {
        let _guard = LEDGER_GUARD.lock().unwrap_or_else(|e| e.into_inner());
        // Unique keys so a concurrent test in this binary can't collide with them.
        reset();
        ran("RuntraceTest::ui");
        ran("RuntraceTest::ui");
        ran("RuntraceTest::ctl:11M");
        let s = snapshot();
        assert_eq!(s["RuntraceTest::ui"].as_u64(), Some(2), "ui ran twice");
        assert_eq!(s["RuntraceTest::ctl:11M"].as_u64(), Some(1), "the 11M button handler ran once");
        assert_eq!(count("RuntraceTest::ctl:11M"), 1);
        assert_eq!(count("never"), 0, "an unrun key reads 0");
        // Our two keys are present (the global total may also carry keys from
        // concurrent deck-render tests — assert our keys, not the global count).
        assert!(s.get("RuntraceTest::ui").is_some() && s.get("RuntraceTest::ctl:11M").is_some(), "both keys recorded: {s}");
        // The snapshot is a real JSON object a JS hook / test reads field-by-field.
        assert!(s.is_object());
    }

    /// Reset really clears a recorded key (test-isolation seam).
    #[test]
    fn reset_clears_the_ledger() {
        let _guard = LEDGER_GUARD.lock().unwrap_or_else(|e| e.into_inner());
        ran("RuntraceReset::a::b");
        assert!(count("RuntraceReset::a::b") >= 1, "the key was recorded before reset");
        reset();
        assert_eq!(count("RuntraceReset::a::b"), 0, "reset clears the recorded key");
    }
}