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

nordisk/facett

facett-graph3d/tests/model_generation.rs

//! **Every path that moves the cloud must bump the model version.**
//!
//! A GPU host uploads the cloud once and then writes a 48-byte camera per frame,
//! re-uploading only when `Graph3D::generation()` moves. That is the whole saving —
//! and it is also the whole risk: a mutation that forgets to bump leaves the host
//! drawing a stale cloud for as long as nothing else changes.
//!
//! So this asserts the bump per path, one test per path, so a failure names the
//! mutator rather than saying "something didn't bump". The `nodes` field is `pub` and
//! a counter cannot see a write to a public field, so the two in-tree callers that go
//! through that seam (`codevault::set_positions`, facet-wrapped's `Given` path) call
//! `touch()` explicitly — the last test here pins that contract.

use egui::Color32;
use facett_graph3d::{Graph3D, Layout3D};

fn sample() -> Graph3D {
    let nodes: Vec<(String, Color32)> =
        (0..12).map(|i| (format!("n{i}"), Color32::from_rgb(10, 120, 200))).collect();
    let edges: Vec<(usize, usize)> = (0..12).map(|i| (i, (i + 1) % 12)).collect();
    Graph3D::new(nodes, edges, Layout3D::Sphere)
}

/// `layout()` repositions every node — the most obvious bump, and the one every other
/// mutator routes through.
#[test]
fn layout_bumps_the_generation() {
    let mut g = sample();
    let before = g.generation();
    g.layout();
    assert!(g.generation() > before, "layout() must bump: {before} -> {}", g.generation());
}

/// `set_positions` changes the host placement even when the active layout is not
/// `Given` (it is stored and takes effect on the next switch), so it must bump either
/// way — the case an exit-side bump inside the `if` would have missed.
#[test]
fn set_positions_bumps_even_when_the_layout_is_not_given() {
    let mut g = sample();
    assert_ne!(g.state().layout, Layout3D::Given, "the sample is deliberately not Given");
    let before = g.generation();
    g.set_positions(vec![[0.1, 0.2, 0.3]; 12]);
    assert!(
        g.generation() > before,
        "set_positions must bump even off the Given path: {before} -> {}",
        g.generation()
    );
}

/// `absorb` adds nodes and edges — the model changes shape, not just position.
///
/// **What this does and does NOT prove.** It is a CONTRACT test: after `absorb`, the
/// version moved, which is all a GPU host needs. It is *not* sensitive to deleting
/// `absorb`'s own `self.touch()`, because `absorb` calls `layout()` internally and that
/// bumps too — driven red, and it stayed green. The explicit `touch()` is kept as belt
/// and braces for the day `absorb` stops routing through `layout`, but its passing here
/// is evidence about the OUTCOME, not about that line. Said plainly rather than left to
/// look sharper than it is.
#[test]
fn absorb_bumps_the_generation() {
    let mut g = sample();
    let before = g.generation();
    g.absorb(vec![("new".into(), Color32::RED)], vec![(0, 12)]);
    assert!(g.generation() > before, "absorb() must bump: {before} -> {}", g.generation());
}

/// A camera move is NOT a model change. This is the half that makes the optimisation
/// worth anything: if spinning bumped the version, the host would re-upload every
/// frame and the whole scheme would be the instance lane with extra steps.
#[test]
fn spinning_the_camera_does_not_bump() {
    let mut g = sample();
    g.layout();
    let before = g.generation();
    for _ in 0..30 {
        g.tick(0.016);
    }
    g.set_spin_rate(0.9);
    g.select(Some(3));
    assert_eq!(
        g.generation(),
        before,
        "camera/selection changes must NOT bump the model version — re-uploading on a \
         spin is exactly the cost this counter exists to avoid"
    );
}

/// The cloud model is what actually gets uploaded, so it must match the graph: one
/// point per node, and edges dropped only when they name a node that does not exist.
#[cfg(feature = "wgpu")]
#[test]
fn the_cloud_model_matches_the_graph() {
    let mut g = sample();
    g.layout();
    let pts = g.cloud_points();
    let eds = g.cloud_edges();
    assert_eq!(pts.len(), g.nodes.len(), "one CloudPoint per node");
    assert_eq!(eds.len(), g.edges.len(), "every in-range edge survives");
    // The positions really are the graph's, not zeros — a model of 12 origin points
    // would satisfy a length check and render nothing.
    for (p, n) in pts.iter().zip(&g.nodes) {
        assert_eq!(p.pos, n.pos, "the cloud point carries the node's position");
    }
    assert!(
        pts.iter().any(|p| p.pos.iter().any(|c| c.abs() > 1e-3)),
        "the laid-out model is not all origin"
    );
    // Colour survives the RGBA8 pack.
    let [r, gg, b, a] = g.nodes[0].color.to_array();
    let want = u32::from(r) | (u32::from(gg) << 8) | (u32::from(b) << 16) | (u32::from(a) << 24);
    assert_eq!(pts[0].color, want, "colour packs little-endian RGBA8");
}

/// **The public-seam contract.** `nodes` is `pub`, so a caller can move a node without
/// the counter noticing; the documented rule is that such a caller calls `touch()`.
/// This pins both halves: a raw write alone does NOT bump (so the hazard is real and
/// documented honestly, not papered over), and `touch()` does.
#[test]
fn a_raw_seam_write_needs_touch_and_touch_works() {
    let mut g = sample();
    g.layout();
    let before = g.generation();
    g.nodes[0].pos = [0.9, 0.9, 0.9];
    assert_eq!(
        g.generation(),
        before,
        "a write through the pub `nodes` seam is invisible to the counter — this is the \
         known hazard the field's doc names, asserted so it stays known"
    );
    g.touch();
    assert!(g.generation() > before, "touch() is how such a caller declares the change");
}