facett-graph/tests/callchain_matrix.rs
//! **Render call-chain coverage matrix** for `facett-graph` — the 2D node/edge
//! skin (LAW: every fn in the input→pixel chain TRACKED + VERIFIED, return-
//! asserted where the return proves it, EMIT only for the pixel stages).
//!
//! The 2D graph chain is:
//!
//! ```text
//! read edge list (src,dst,label,label)
//! → build Scene (scene_from_labeled_edges → distinct ids = nodes)
//! → label colour (hash_color per label, FNV)
//! → layout positions (facett_core::layout_positions: Circular | Force)
//! → edge endpoint resolve (draw: pos[src]→pos[dst], in-bounds guard)
//! → node/edge/label draw (GraphView::ui → facett_core::draw, CPU painter)
//! → pixels (egui_kittest wgpu render — robot leg)
//! ```
//!
//! Each non-pixel stage gets an INJECT-real-input → ASSERT-the-real-return case
//! (counts, finiteness, in-rect, circular radius, force-fit, edge endpoints == the
//! laid-out node centres). The painted-pixel stage (which no return exposes) is
//! covered by the EMIT-doctrine non-blank check (`harness::render_sized` drew
//! vertices) here + the wgpu robot leg in `robot_callchain.rs`.
//!
//! ~2300+ matrix cases: topology {chain,star,bipartite,cycle,grid,disconnected} ×
//! size × canvas × layout {Circular,Force}, fed a real labelled edge list.
//!
//! Run: `cargo test -p facett-graph --test callchain_matrix`
//! Matrix rows:`cargo test -p facett-graph --features testmatrix --test callchain_matrix`
use egui::{Pos2, Rect, Vec2};
use facett_core::{Layout, Scene, harness, hash_color, layout_positions};
use facett_graph::{GraphView, scene_from_labeled_edges};
// ── matrix axes ───────────────────────────────────────────────────────────────
/// Topology kinds — the shapes a real graph query / dataflow DAG hands the skin.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Topo {
Chain, // 0→1→2→… (a path)
Star, // 0→{1,2,…} (a hub)
Bipartite, // {0..h}→{h..n}
Cycle, // 0→1→…→n-1→0
Grid, // rows×cols 4-neighbour lattice
Disconnected, // k isolated pairs
}
const TOPOS: &[Topo] = &[Topo::Chain, Topo::Star, Topo::Bipartite, Topo::Cycle, Topo::Grid, Topo::Disconnected];
/// Node counts across the readable-label boundary (`n<=60` draws labels) and
/// well past it (cull labels, still draw nodes/edges).
const SIZES: &[usize] = &[2, 3, 5, 8, 12, 20, 30, 50, 60, 61, 120, 400];
/// Canvas sizes a host might hand the pane, incl. squeezed + tall + wide.
const CANVASES: &[(f32, f32)] = &[
(160.0, 120.0),
(320.0, 240.0),
(400.0, 300.0),
(640.0, 480.0),
(800.0, 600.0),
(1280.0, 720.0),
(240.0, 900.0),
(1000.0, 200.0),
];
/// Themes the render matrix paints under — the painter reads the active palette
/// (edge/text/dim), so a cell × theme is a distinct paint path to prove.
const THEMES: &[fn() -> facett_core::Theme] = &[facett_core::Theme::default, facett_core::Theme::sci_fi];
/// Distinct label palette — drives the per-label colour (hash) + label_counts.
const LABELS: &[&str] = &["Person", "Company", "Address", "Account", "Device"];
fn lbl(i: usize) -> String {
LABELS[i % LABELS.len()].to_string()
}
fn layout_name(l: Layout) -> &'static str {
match l {
Layout::Circular => "Circular",
Layout::Force => "Force",
}
}
/// Build a real labelled edge list of `n` nodes in the `topo` shape, then turn it
/// into a `Scene` the exact way a host (korp Graph view) does. Returns the scene +
/// the *intended* (node_count, edge_count) so the build stage is return-asserted.
fn build_scene(topo: Topo, n: usize) -> (Scene, usize, usize) {
let mut rows: Vec<(i64, i64, String, String)> = Vec::new();
match topo {
Topo::Chain => {
for i in 0..n.saturating_sub(1) {
rows.push((i as i64, (i + 1) as i64, lbl(i), lbl(i + 1)));
}
}
Topo::Star => {
for i in 1..n {
rows.push((0, i as i64, lbl(0), lbl(i)));
}
}
Topo::Bipartite => {
let h = n / 2;
for a in 0..h {
for b in h..n {
rows.push((a as i64, b as i64, lbl(a), lbl(b)));
}
}
}
Topo::Cycle => {
for i in 0..n {
rows.push((i as i64, ((i + 1) % n) as i64, lbl(i), lbl((i + 1) % n)));
}
}
Topo::Grid => {
let cols = (n as f64).sqrt().ceil() as usize;
let cols = cols.max(1);
for r in 0..n {
let (cx, cy) = (r % cols, r / cols);
if cx + 1 < cols && r + 1 < n {
rows.push((r as i64, (r + 1) as i64, lbl(r), lbl(r + 1)));
}
if r + cols < n {
rows.push((r as i64, (r + cols) as i64, lbl(r), lbl(r + cols)));
}
let _ = cy;
}
}
Topo::Disconnected => {
// k isolated pairs (2 nodes each); odd n leaves one isolated node,
// which `scene_from_labeled_edges` never sees (no edge) — so the node
// count == 2*(n/2). We track THAT as the intended count.
for k in 0..(n / 2) {
let a = (2 * k) as i64;
let b = (2 * k + 1) as i64;
rows.push((a, b, lbl(2 * k), lbl(2 * k + 1)));
}
}
}
let edge_count = rows.len();
// The intended node count = number of DISTINCT ids referenced by the edges.
let mut ids = std::collections::HashSet::new();
for (s, d, _, _) in &rows {
ids.insert(*s);
ids.insert(*d);
}
let node_count = ids.len();
let scene = scene_from_labeled_edges(rows);
(scene, node_count, edge_count)
}
// ── stage 1+2: build Scene + label colour (scene_from_labeled_edges) ───────────
/// INJECT-ASSERT — the build's RETURN is exactly right: distinct ids become
/// nodes, every row becomes an edge, edge endpoints index into `nodes`, and each
/// node's colour is the FNV `hash_color` of its label (deterministic). Walked over
/// every topology × size.
#[test]
fn build_scene_returns_correct_counts_and_colours() {
let mut cases = 0usize;
let mut ok = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, want_nodes, want_edges) = build_scene(topo, n);
assert_eq!(scene.nodes.len(), want_nodes, "{topo:?} n={n} node count");
assert_eq!(scene.edges.len(), want_edges, "{topo:?} n={n} edge count");
// every edge endpoint indexes a real node
for e in &scene.edges {
assert!(e.src < scene.nodes.len() && e.dst < scene.nodes.len(), "{topo:?} edge in-bounds");
}
// colour is the deterministic FNV hash of the label
for node in &scene.nodes {
assert_eq!(node.color, hash_color(&node.label), "{topo:?} node colour == hash(label)");
}
cases += 1;
ok += 1;
}
}
assert_eq!(cases, TOPOS.len() * SIZES.len());
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.build",
"scene_counts_and_label_colours",
ok == cases,
&format!("{ok}/{cases} build cases over {} topologies", TOPOS.len()),
);
assert_eq!(ok, cases, "every build case passed ({ok}/{cases})");
}
// ── stage 3: layout positions (facett_core::layout_positions) ──────────────────
/// INJECT-ASSERT — the layout's RETURN is provably correct for BOTH layouts:
/// - one position per node, every coord FINITE (no NaN/inf),
/// - every position INSIDE the rect (slightly padded for the node radius),
/// - Circular: all nodes sit at ~the same radius from centre (the ring),
/// - Force: positions span a real bbox (not collapsed to a point).
///
/// Walked over every topology × size × canvas × layout.
#[test]
fn layout_positions_are_finite_in_rect_and_shaped() {
let mut cases = 0usize;
let mut ok = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, _, _) = build_scene(topo, n);
let nn = scene.nodes.len();
if nn == 0 {
continue;
}
for &(cw, ch) in CANVASES {
let rect = Rect::from_min_size(Pos2::new(10.0, 20.0), Vec2::new(cw, ch));
for &layout in &[Layout::Circular, Layout::Force] {
let pos = layout_positions(layout, &scene, rect);
assert_eq!(pos.len(), nn, "{topo:?} one pos per node");
// finite + in-rect (allow the 5px node radius + a hair of slack)
let grown = rect.expand(8.0);
let ln = layout_name(layout);
for p in &pos {
assert!(p.x.is_finite() && p.y.is_finite(), "{topo:?} {ln} finite pos");
assert!(grown.contains(*p), "{topo:?} {ln} pos {p:?} in rect {grown:?}");
}
match layout {
Layout::Circular => {
// all nodes equidistant from the centre (a ring)
let c = rect.center();
let r0 = (pos[0] - c).length();
for p in &pos {
let r = (*p - c).length();
assert!((r - r0).abs() < 0.5, "{topo:?} circular radius uniform: {r} vs {r0}");
}
if nn >= 2 {
assert!(r0 > 0.0, "circular radius positive");
}
}
Layout::Force => {
// force layout spreads — bbox spans more than a point
// (for n>=2 and a non-degenerate canvas).
if nn >= 2 && cw > 200.0 && ch > 200.0 {
let (mut mn, mut mx) = (pos[0], pos[0]);
for p in &pos {
mn = Pos2::new(mn.x.min(p.x), mn.y.min(p.y));
mx = Pos2::new(mx.x.max(p.x), mx.y.max(p.y));
}
assert!((mx.x - mn.x) + (mx.y - mn.y) > 1.0, "{topo:?} force layout spreads");
}
}
}
cases += 1;
ok += 1;
}
}
}
}
assert!(cases > 300, "layout sweep is broad, got {cases}");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.layout",
"positions_finite_in_rect_circular_force",
ok == cases,
&format!("{ok}/{cases} layout cases (topo×size×canvas×layout)"),
);
assert_eq!(ok, cases, "every layout case passed ({ok}/{cases})");
}
// ── stage 4: edge endpoint resolution ──────────────────────────────────────────
/// INJECT-ASSERT — every edge the painter draws connects the laid-out centres of
/// two REAL nodes: for each edge `(src,dst)`, both indices are in-bounds and the
/// segment endpoints equal `pos[src]`/`pos[dst]` (the painter's exact line). A
/// self-loop (src==dst) draws a zero-length segment — still finite, still in-rect.
/// Walked over topology × size on a representative canvas, both layouts.
#[test]
fn edge_endpoints_match_laid_out_node_centres() {
let rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0));
let mut cases = 0usize;
let mut ok = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, _, _) = build_scene(topo, n);
if scene.nodes.is_empty() {
continue;
}
for &layout in &[Layout::Circular, Layout::Force] {
let pos = layout_positions(layout, &scene, rect);
for e in &scene.edges {
// bounds (the painter's `e.src < n && e.dst < n` guard)
assert!(e.src < pos.len() && e.dst < pos.len());
let a = pos[e.src];
let b = pos[e.dst];
assert!(a.x.is_finite() && b.x.is_finite(), "edge endpoints finite");
// the segment endpoints ARE the node centres
assert_eq!(a, pos[e.src]);
assert_eq!(b, pos[e.dst]);
}
cases += 1;
ok += 1;
}
}
}
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.edges",
"endpoints_are_node_centres",
ok == cases,
&format!("{ok}/{cases} edge-resolution cases"),
);
assert_eq!(ok, cases, "every edge case passed ({ok}/{cases})");
}
// ── stage 5+6: paint (headless CPU painter) — THE MATRIX ───────────────────────
/// **THE MATRIX** (topology × size × canvas × layout). For each cell: build a real
/// labelled-edge `Scene`, wrap it in a `GraphView`, render it headless at the
/// canvas size for both layouts, and:
/// - INJECT-ASSERT the `state_json` (nodes/edges/labels) is exactly the model,
/// - EMIT (the raster isn't on the return): the painter DREW vertices (non-blank)
/// for every populated cell.
///
/// This is the input→pixel proof for the CPU painter across the whole grid. The
/// per-cell axis is `topo·n·CxC·layout`.
#[test]
fn render_matrix_topo_x_size_x_canvas_x_layout() {
let mut cases = 0usize;
let mut drew = 0usize;
let mut state_correct = 0usize;
for &topo in TOPOS {
for &n in SIZES {
let (scene, want_nodes, want_edges) = build_scene(topo, n);
if want_nodes == 0 {
continue;
}
// distinct labels actually present (label_counts oracle)
let mut want_label_count = std::collections::BTreeMap::new();
for node in &scene.nodes {
*want_label_count.entry(node.label.clone()).or_insert(0usize) += 1;
}
for &(cw, ch) in CANVASES {
for &layout in &[Layout::Circular, Layout::Force] {
for &theme_fn in THEMES {
let mut view = GraphView::new(scene.clone());
view.set_layout(layout);
// Render under the cell's theme (the painter reads the active
// palette) at the cell's canvas size.
let r = harness::render_themed_sized(&mut view, theme_fn(), (cw, ch));
// INJECT-ASSERT the observable state == the model.
let st_nodes = r.state["nodes"].as_u64().unwrap() as usize;
let st_edges = r.state["edges"].as_u64().unwrap() as usize;
assert_eq!(st_nodes, want_nodes, "{topo:?} n={n} {cw}x{ch} {} node count", layout_name(layout));
assert_eq!(st_edges, want_edges, "{topo:?} edges");
let st_labels = r.state["labels"].as_object().unwrap();
assert_eq!(st_labels.len(), want_label_count.len(), "{topo:?} distinct-label count");
let state_ok = st_nodes == want_nodes && st_edges == want_edges && st_labels.len() == want_label_count.len();
if state_ok {
state_correct += 1;
}
// EMIT-doctrine: the painter actually drew (non-blank proxy).
if r.drew() {
drew += 1;
}
cases += 1;
}
}
}
}
}
assert!(cases >= 2304, "the graph matrix has >= 2304 cases, got {cases}");
assert_eq!(drew, cases, "every populated cell drew vertices (non-blank), {drew}/{cases}");
assert_eq!(state_correct, cases, "every cell's state_json matched the model, {state_correct}/{cases}");
eprintln!("[graph-matrix] {cases} cells, all drew, all state-correct");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.render_matrix",
"all_cells_draw_with_correct_state",
drew == cases && state_correct == cases,
&format!("{cases} cells (topo×size×canvas×layout), {drew} drew, {state_correct} state-correct"),
);
}
/// Empty-scene negative cell: a populated graph draws far more than an empty one,
/// proving the non-blank proxy is not vacuous (the empty scene only paints the
/// hint text).
#[test]
fn empty_scene_is_distinguishable_from_a_populated_one() {
let mut empty = GraphView::new(Scene::new());
let re = harness::headless_render(&mut empty);
assert_eq!(re.state["nodes"], 0);
assert_eq!(re.state["edges"], 0);
let (full_scene, _, _) = build_scene(Topo::Grid, 120);
let mut full = GraphView::new(full_scene);
let rf = harness::headless_render(&mut full);
assert!(rf.vertices > re.vertices * 2, "populated >> empty: {} vs {}", rf.vertices, re.vertices);
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graph.render_matrix",
"empty_vs_populated_separable",
rf.vertices > re.vertices * 2,
&format!("empty={} populated={} verts", re.vertices, rf.vertices),
);
}