facett-core/src/runtrace.rs
//! **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");
}
}