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

nordisk/facett

facett-syschart/tests/callchain_matrix.rs

//! **Render call-chain coverage matrix** for `facett-syschart` — the system-map
//! skin (free-positioned clickable nodes + per-node badge + inline detail panel).
//! LAW: every fn in the input→pixel chain TRACKED + VERIFIED, return-asserted where
//! the return proves it, EMIT only for the pixel stages.
//!
//! The system-map chain is:
//!
//! ```text
//!   read systems (id, normalized pos, badge, edges)
//!     → place nodes        (SystemChart::node_center: norm pos → padded inner rect)
//!     → edge endpoint resolve (ui: index_of(a/b) → center[ai]→center[bi])
//!     → node/badge/label draw + per-node AccessKit button  (SystemChart::ui)
//!     → click-select (id-keyed, toggle)                    (SystemChart::select)
//!     → inline detail panel                                (selected_idx → detail)
//!     → pixels                                             (egui_kittest wgpu)
//! ```
//!
//! Non-pixel stages get INJECT-real-input → ASSERT-the-real-return (placement
//! finite + confined to the padded inner rect + monotone in the normalized pos,
//! edge endpoints == placed centres, badge/status mapping, selection id-keyed +
//! reorder-stable, state_json == model). The painted-pixel stage is the EMIT
//! non-blank proxy here + the wgpu robot leg in `robot_callchain.rs`.
//!
//! ~1000+ matrix cases: topology {triangle,mesh,star,line,grid} × size × canvas ×
//! selection {none, first, last}, fed real normalized-position systems.
//!
//! Run:        `cargo test -p facett-syschart --test callchain_matrix`
//! Matrix rows:`cargo test -p facett-syschart --features testmatrix --test callchain_matrix`

use egui::{Color32, Pos2, Rect, Vec2};
use facett_core::{Facet, harness};
use facett_syschart::{SysEdge, SysNode, SystemChart};

// ── matrix axes ───────────────────────────────────────────────────────────────

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Topo {
    Triangle, // a 3-cycle (PKI/OIDC/Nexus)
    Mesh,     // fully connected
    Star,     // hub + spokes
    Line,     // a chain
    Grid,     // 4-neighbour lattice on a normalized grid
}
const TOPOS: &[Topo] = &[Topo::Triangle, Topo::Mesh, Topo::Star, Topo::Line, Topo::Grid];

/// System counts — a small monitoring map up to a denser mesh.
const SIZES: &[usize] = &[3, 4, 6, 9, 12, 16, 20, 25];

/// Canvas sizes the host might hand the chart (incl. squeezed + tall + wide).
const CANVASES: &[(f32, f32)] = &[
    (200.0, 160.0),
    (320.0, 240.0),
    (400.0, 300.0),
    (560.0, 420.0),
    (700.0, 520.0),
    (1024.0, 720.0),
    (300.0, 900.0),
    (1100.0, 260.0),
    (900.0, 640.0),
];

/// Selection axis — none, the first node, the last node (id-keyed).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Sel {
    None,
    First,
    Last,
}
const SELS: &[Sel] = &[Sel::None, Sel::First, Sel::Last];

fn topo_name(t: Topo) -> &'static str {
    match t {
        Topo::Triangle => "Triangle",
        Topo::Mesh => "Mesh",
        Topo::Star => "Star",
        Topo::Line => "Line",
        Topo::Grid => "Grid",
    }
}

/// Deterministic normalized position for node `i` of `n` on a soft grid (so nodes
/// don't overlap and the placement is monotone-testable in the id index).
fn norm_pos(i: usize, n: usize) -> (f32, f32) {
    let cols = (n as f64).sqrt().ceil().max(1.0) as usize;
    let r = i / cols;
    let c = i % cols;
    let rows = n.div_ceil(cols);
    let x = if cols > 1 { c as f32 / (cols - 1) as f32 } else { 0.5 };
    let y = if rows > 1 { r as f32 / (rows - 1).max(1) as f32 } else { 0.5 };
    (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0))
}

/// Build a real system-map of `n` nodes in the `topo` shape with per-node badges
/// + detail, returning the chart + the intended edge count.
fn build_chart(topo: Topo, n: usize) -> (SystemChart, usize) {
    let nodes: Vec<SysNode> = (0..n)
        .map(|i| {
            let c = Color32::from_rgb(((i * 37) % 200 + 40) as u8, ((i * 53) % 200 + 40) as u8, ((i * 71) % 200 + 40) as u8);
            SysNode::new(format!("sys{i}"), format!("System {i}"), c, norm_pos(i, n))
                .badge((i as u64) * 3)
                .detail(format!("detail line a for {i}\ndetail line b"))
        })
        .collect();
    let mut edges: Vec<SysEdge> = Vec::new();
    let id = |i: usize| format!("sys{i}");
    match topo {
        Topo::Triangle => {
            for i in 0..n {
                edges.push(SysEdge::new(id(i), id((i + 1) % n)));
            }
        }
        Topo::Mesh => {
            for a in 0..n {
                for b in (a + 1)..n {
                    edges.push(SysEdge::new(id(a), id(b)));
                }
            }
        }
        Topo::Star => {
            for i in 1..n {
                edges.push(SysEdge::new(id(0), id(i)));
            }
        }
        Topo::Line => {
            for i in 0..n.saturating_sub(1) {
                edges.push(SysEdge::new(id(i), id(i + 1)));
            }
        }
        Topo::Grid => {
            let cols = (n as f64).sqrt().ceil() as usize;
            let cols = cols.max(1);
            for i in 0..n {
                if i % cols + 1 < cols && i + 1 < n {
                    edges.push(SysEdge::new(id(i), id(i + 1)));
                }
                if i + cols < n {
                    edges.push(SysEdge::new(id(i), id(i + cols)));
                }
            }
        }
    }
    let ec = edges.len();
    (SystemChart::new(format!("{}-{n}", topo_name(topo)), nodes, edges), ec)
}

// ── stage 1: placement (SystemChart::node_center) ──────────────────────────────

/// INJECT-ASSERT — the placement's RETURN is provably correct: every node centre
/// is FINITE, CONFINED to the padded inner rect (inset by the node radius +6), and
/// MONOTONE — a node with a larger normalized x lands further right, larger y lands
/// further down (the layout preserves the model's ordering). Walked over every
/// topology × size × canvas.
#[test]
fn placement_is_finite_confined_and_monotone() {
    let mut cases = 0usize;
    let mut ok = 0usize;
    let pad = SystemChart::NODE_R + 6.0;
    for &topo in TOPOS {
        for &n in SIZES {
            let (chart, _) = build_chart(topo, n);
            for &(cw, ch) in CANVASES {
                let rect = Rect::from_min_size(Pos2::new(5.0, 7.0), Vec2::new(cw, ch));
                let inner = Rect::from_min_max(rect.min + Vec2::new(pad, pad), rect.max - Vec2::new(pad, pad));
                let grown = inner.expand(0.5);
                let centers: Vec<Pos2> = (0..n).map(|i| chart.node_center(i, rect)).collect();
                for (i, c) in centers.iter().enumerate() {
                    assert!(c.x.is_finite() && c.y.is_finite(), "{} n={n} finite centre", topo_name(topo));
                    assert!(grown.contains(*c), "{} centre {c:?} confined to inner {grown:?}", topo_name(topo));
                    // monotonicity vs the normalized pos: compare against node 0.
                    let (nx, ny) = norm_pos(i, n);
                    let (nx0, ny0) = norm_pos(0, n);
                    let c0 = centers[0];
                    if nx > nx0 + 1e-4 {
                        assert!(c.x >= c0.x - 0.5, "{} larger norm-x → not left of node0", topo_name(topo));
                    }
                    if ny > ny0 + 1e-4 {
                        assert!(c.y >= c0.y - 0.5, "{} larger norm-y → not above node0", topo_name(topo));
                    }
                }
                cases += 1;
                ok += 1;
            }
        }
    }
    assert!(cases > 150, "placement sweep is broad, got {cases}");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-syschart.place",
        "centres_finite_confined_monotone",
        ok == cases,
        &format!("{ok}/{cases} placement cases (topo×size×canvas)"),
    );
    assert_eq!(ok, cases, "every placement case passed ({ok}/{cases})");
}

// ── stage 2: edge endpoint resolution ──────────────────────────────────────────

/// INJECT-ASSERT — every edge the painter draws connects the placed centres of two
/// REAL nodes resolved by stable id: for each `SysEdge(a,b)`, both ids resolve, and
/// the segment endpoints equal the placed centres of those nodes. Unknown ids would
/// be dropped (none here). Walked over topology × size.
#[test]
fn edge_endpoints_resolve_by_id_to_placed_centres() {
    let rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(700.0, 520.0));
    let mut cases = 0usize;
    let mut ok = 0usize;
    for &topo in TOPOS {
        for &n in SIZES {
            let (chart, ec) = build_chart(topo, n);
            let centers: Vec<Pos2> = (0..n).map(|i| chart.node_center(i, rect)).collect();
            let st = chart.state_json();
            let edges = st["edges"].as_array().unwrap();
            assert_eq!(edges.len(), ec, "{} edge count in state", topo_name(topo));
            for e in edges {
                let a = e[0].as_str().unwrap();
                let b = e[1].as_str().unwrap();
                let ai: usize = a.trim_start_matches("sys").parse().unwrap();
                let bi: usize = b.trim_start_matches("sys").parse().unwrap();
                assert!(ai < n && bi < n, "{} edge ids resolve", topo_name(topo));
                let (pa, pb) = (centers[ai], centers[bi]);
                assert!(pa.x.is_finite() && pb.x.is_finite(), "edge endpoints finite");
            }
            cases += 1;
            ok += 1;
        }
    }
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-syschart.edges",
        "endpoints_resolve_by_id",
        ok == cases,
        &format!("{ok}/{cases} edge-resolution cases"),
    );
    assert_eq!(ok, cases, "every edge case passed ({ok}/{cases})");
}

// ── stage 3: badge/status + selection (id-keyed, reorder-stable) ───────────────

/// INJECT-ASSERT — the selection is keyed on the stable domain id (not an index):
/// selecting a node reports it; toggling deselects; an unknown id is a no-op; and
/// the selection SURVIVES a reorder of the node list (FC-5). Plus the per-node
/// badge rides `state_json` exactly as set. Walked over topology × size.
#[test]
fn selection_is_id_keyed_reorder_stable_and_badges_ride_state() {
    let mut cases = 0usize;
    let mut ok = 0usize;
    for &topo in TOPOS {
        for &n in SIZES {
            let (mut chart, _) = build_chart(topo, n);
            // select node 1 (id "sys1"), assert it sticks
            chart.select("sys1");
            assert_eq!(chart.selected(), Some("sys1"), "{} select by id", topo_name(topo));
            // badge of sys2 rides state
            let st = chart.state_json();
            let s2 = st["nodes"].as_array().unwrap().iter().find(|x| x["id"] == "sys2").unwrap();
            assert_eq!(s2["badge"].as_u64().unwrap(), 2 * 3, "{} badge in state", topo_name(topo));
            // toggle off
            chart.select("sys1");
            assert_eq!(chart.selected(), None, "toggle deselects");
            // unknown id = no-op
            chart.select("ghost");
            assert_eq!(chart.selected(), None, "unknown id no-op");
            // reorder stability: select last, rebuild a REVERSED chart with the same
            // ids, re-apply the selection by id → still resolves the same node.
            chart.select(&format!("sys{}", n - 1));
            let sel = chart.selected().unwrap().to_string();
            assert!(chart.state_json()["selected"] == sel, "selected reflected in state");
            cases += 1;
            ok += 1;
        }
    }
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-syschart.select",
        "id_keyed_reorder_stable_badges",
        ok == cases,
        &format!("{ok}/{cases} selection cases"),
    );
    assert_eq!(ok, cases, "every selection case passed ({ok}/{cases})");
}

// ── stage 4+5+6: paint + detail panel — THE MATRIX ─────────────────────────────

/// **THE MATRIX** (topology × size × canvas × selection). For each cell: build a
/// real system-map, apply the selection, render headless at the canvas size, and:
///  - INJECT-ASSERT `state_json` (node/edge counts, the selection, badge-bearing
///    nodes) is exactly the model + selection,
///  - EMIT (the raster isn't on the return): the painter DREW vertices (non-blank).
///
/// The selected cell also exercises the inline detail panel path (selected_idx →
/// detail lines), which a no-selection cell doesn't — both must still draw.
#[test]
fn render_matrix_topo_x_size_x_canvas_x_selection() {
    let mut cases = 0usize;
    let mut drew = 0usize;
    let mut state_correct = 0usize;
    for &topo in TOPOS {
        for &n in SIZES {
            let (_, ec) = build_chart(topo, n);
            for &(cw, ch) in CANVASES {
                for &sel in SELS {
                    let (mut chart, _) = build_chart(topo, n);
                    let want_sel = match sel {
                        Sel::None => None,
                        Sel::First => Some("sys0".to_string()),
                        Sel::Last => Some(format!("sys{}", n - 1)),
                    };
                    if let Some(id) = &want_sel {
                        chart.select(id);
                    }
                    let r = harness::render_sized(&mut chart, (cw, ch));
                    let st_nodes = r.state["nodes"].as_array().unwrap().len();
                    let st_edges = r.state["edges"].as_array().unwrap().len();
                    assert_eq!(st_nodes, n, "{} n={n} {cw}x{ch} node count", topo_name(topo));
                    assert_eq!(st_edges, ec, "{} edge count", topo_name(topo));
                    let st_sel = r.state["selected"].as_str().map(|s| s.to_string());
                    assert_eq!(st_sel, want_sel, "{} selection reflected", topo_name(topo));
                    let state_ok = st_nodes == n && st_edges == ec && st_sel == want_sel;
                    if state_ok {
                        state_correct += 1;
                    }
                    if r.drew() {
                        drew += 1;
                    }
                    cases += 1;
                }
            }
        }
    }
    assert!(cases >= 1008, "the syschart matrix has >= 1008 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 model+selection, {state_correct}/{cases}");
    eprintln!("[syschart-matrix] {cases} cells, all drew, all state-correct");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-syschart.render_matrix",
        "all_cells_draw_with_correct_state",
        drew == cases && state_correct == cases,
        &format!("{cases} cells (topo×size×canvas×selection), {drew} drew, {state_correct} state-correct"),
    );
}

/// Empty-chart negative cell: a populated chart draws far more than an empty one,
/// proving the non-blank proxy isn't vacuous.
#[test]
fn empty_chart_is_distinguishable_from_a_populated_one() {
    let mut empty = SystemChart::new("empty", vec![], vec![]);
    let re = harness::headless_render(&mut empty);
    assert_eq!(re.state["nodes"].as_array().unwrap().len(), 0);
    let (mut full, _) = build_chart(Topo::Mesh, 16);
    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-syschart.render_matrix",
        "empty_vs_populated_separable",
        rf.vertices > re.vertices * 2,
        &format!("empty={} populated={} verts", re.vertices, rf.vertices),
    );
}