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

nordisk/facett

facett-simview/tests/robot.rs

//! **The Simulate viewer, driven headless against the mock contract** (facett robot
//! style: every assertion reads `state_json` or the request log, never pixels).
//!
//! `cargo test -p facett-simview --test robot`

use std::cell::RefCell;

use facett_core::{harness, Facet};
use facett_graph3d::Msg as G3Msg;
use facett_simview::contract::{Req, Resp, VERB_GENERATE, VERB_PERMUTATE};
use facett_simview::mock::{MockBackend, MockServer};
use facett_simview::{Backend, SimView, POLL_S};

const PERSONS: usize = 5_000;

fn loaded() -> (SimView, MockBackend) {
    let mock = MockBackend::new(MockServer::new(PERSONS));
    let mut v = SimView::new(Box::new(mock.clone()));
    v.step(0.0);
    (v, mock)
}

/// Step the clock past each poll until the job is over and the refetch has landed.
fn run_job(v: &mut SimView, mut now: f64) -> f64 {
    for _ in 0..20 {
        now += POLL_S + 0.1;
        v.step(now);
        v.step(now); // the answers to what that step sent
        let st = v.state_json();
        if !st["busy"].as_bool().unwrap() && v.in_flight() == 0 {
            return now;
        }
    }
    panic!("the job never ended: {}", v.state_json());
}

fn count(log: &[Req], f: impl Fn(&Req) -> bool) -> usize {
    log.iter().filter(|r| f(r)).count()
}

#[test]
fn the_3d_view_holds_every_person_and_lights_the_latest_round() {
    let (mut v, mock) = loaded();
    let st = v.state_json();
    assert_eq!(st["nodes"], PERSONS, "{st}");
    assert_eq!(st["graph3d_nodes"], PERSONS, "the 3D view holds every person");
    assert_eq!(st["layout"], "given", "placed by kommun, not by a force layout");
    // Round 0: everybody is the latest round.
    assert_eq!(st["latest_round"], 0);
    assert_eq!(st["highlighted"], mock.server.lock().unwrap().changed_in_latest_round());

    // Permutate 10% → round 1 → exactly the moved tenth is lit.
    v.update_json(r#"{"press":"permutate"}"#);
    v.step(0.1);
    run_job(&mut v, 0.1);
    let st = v.state_json();
    let changed = mock.server.lock().unwrap().changed_in_latest_round();
    assert_eq!(changed, PERSONS / 10);
    assert_eq!(st["latest_round"], 1, "{st}");
    assert_eq!(st["highlighted"], changed, "highlighted == persons with round == latest round: {st}");
    // And the paint says so: that many nodes carry the lit colour.
    let lit = v.graph().nodes.iter().filter(|n| n.color == facett_simview::LIT).count();
    assert_eq!(lit, changed);
    assert!(st["status"].as_str().unwrap().contains("permutate: done"), "{st}");
}

#[test]
fn selecting_a_node_in_3d_fetches_its_ego_network_into_vg() {
    let (mut v, mock) = loaded();
    let id = 42_i64;
    // A click in the 3D pane is `Msg::SelectNode(index)`; persons are in id order here.
    let _ = v.graph_mut().update(G3Msg::SelectNode(id as usize));
    // The frame notices the selection and asks for the ego network.
    let r = harness::render_sized(&mut v, (1400.0, 800.0));
    assert!(r.drew());
    assert!(mock.log().contains(&Req::Ego(id)), "the selection fetched /sim/api/ego.arrow?id={id}");
    v.step(0.0); // ego edges → asks for the node rows
    assert!(mock.log().iter().any(|r| matches!(r, Req::NodesByIds(ids) if ids.contains(&id))));
    v.step(0.0); // node rows → V.G
    let st = v.state_json();
    assert_eq!(st["selected"], id);
    assert_eq!(st["selected_3d"], id as usize);
    // The mock's person owns four records: the ego is 5 nodes, 4 edges.
    assert_eq!(st["ego"]["of"], id);
    assert_eq!(st["vg"]["nodes"], 5, "{st}");
    assert_eq!(st["vg"]["edges"], 4, "{st}");
    assert_eq!(st["vg"]["selected"], id.to_string());
    // And V.G really renders it.
    let r = harness::render_sized(&mut v, (1400.0, 800.0));
    assert!(r.drew());
    let nav = v.nav().expect("V.G is up");
    assert!(nav.graph.nodes.iter().any(|n| n.id == id.to_string() && n.label == "Person"));
}

#[test]
fn a_typed_id_selects_too() {
    let (mut v, mock) = loaded();
    v.update_json(r#"{"select": 7}"#);
    v.step(0.0);
    v.step(0.0);
    assert!(mock.log().contains(&Req::Ego(7)));
    assert_eq!(v.state_json()["vg"]["nodes"], 5);
    assert_eq!(v.state_json()["selected_3d"], 7);
}

#[test]
fn generate_posts_and_polls_every_two_seconds_until_done_then_refetches() {
    let (mut v, mock) = loaded();
    let before = mock.log().len();
    v.press(VERB_GENERATE);
    v.step(0.1);
    let log = mock.log();
    assert_eq!(log[before], Req::Press(VERB_GENERATE), "Generate is POST /power/falkordb/simulate");
    assert_eq!(Req::Press(VERB_GENERATE).http(), ("POST", "/power/falkordb/simulate".to_string()));
    assert!(v.state_json()["busy"].as_bool().unwrap());
    // No poll before 2 s have passed…
    v.step(1.0);
    v.step(1.5);
    assert_eq!(count(&mock.log()[before..], |r| *r == Req::Power), 0, "no poll inside the 2 s");
    // …then one per 2 s while busy.
    v.step(2.2);
    assert_eq!(count(&mock.log()[before..], |r| *r == Req::Power), 1);
    v.step(2.3);
    v.step(3.0);
    assert_eq!(count(&mock.log()[before..], |r| *r == Req::Power), 1, "one poll per period");
    run_job(&mut v, 2.3);
    let after = &mock.log()[before..];
    let polls = count(after, |r| *r == Req::Power);
    assert!(polls >= 3, "the mock's job is busy for 2 polls: {polls} polls");
    // The job ended ok → meta + persons are fetched again.
    let last_poll = after.iter().rposition(|r| *r == Req::Power).unwrap();
    assert!(after[last_poll..].contains(&Req::Persons), "refetch after the job: {after:?}");
    assert!(after[last_poll..].contains(&Req::Meta));
    assert!(v.state_json()["status"].as_str().unwrap().contains("simulate: done"));
}

#[test]
fn a_second_press_while_running_is_a_409_and_says_so() {
    let (mut v, mock) = loaded();
    v.press(VERB_PERMUTATE);
    v.step(0.1);
    v.press(VERB_GENERATE);
    v.step(0.2);
    let st = v.state_json();
    assert!(st["status"].as_str().unwrap().contains("409"), "{st}");
    assert!(st["busy"].as_bool().unwrap(), "a 409 means a job runs: keep polling");
    assert_eq!(count(&mock.log(), |r| matches!(r, Req::Press(_))), 2);
}

/// A server that answers every press with something that is neither 202 nor 409.
#[derive(Default)]
struct Teapot(RefCell<Vec<Resp>>);
impl Backend for Teapot {
    fn send(&self, req: Req) {
        let (status, body) = match req {
            Req::Press(_) => (418, b"bench is a teapot today".to_vec()),
            _ => (503, b"down for maintenance".to_vec()),
        };
        self.0.borrow_mut().push(Resp { req, status, body });
    }
    fn drain(&self) -> Vec<Resp> {
        std::mem::take(&mut *self.0.borrow_mut())
    }
}

#[test]
fn any_other_status_shows_the_body() {
    let mut v = SimView::new(Box::new(Teapot::default()));
    v.step(0.0);
    assert!(v.state_json()["status"].as_str().unwrap().contains("down for maintenance"));
    v.press(VERB_GENERATE);
    v.step(0.1);
    let st = v.state_json();
    assert!(st["status"].as_str().unwrap().contains("HTTP 418: bench is a teapot today"), "{st}");
    assert!(!st["busy"].as_bool().unwrap(), "not accepted → nothing to poll");
}

#[test]
fn the_cpu_lane_is_named_and_bounded_at_110k() {
    let mock = MockBackend::new(MockServer::new(110_000));
    let mut v = SimView::new(Box::new(mock));
    v.step(0.0);
    let r = harness::render_sized(&mut v, (1600.0, 900.0));
    assert!(r.drew());
    let st = &r.state;
    assert_eq!(st["nodes"], 110_000);
    // No GPU renderer installed in a headless host test: the strided CPU sample.
    if !st["gpu_feature"].as_bool().unwrap() {
        assert_eq!(st["render_lane"], "cpu-strided", "{st}");
        assert!(st["cpu_paint_stride"][0].as_u64().unwrap() > 1, "{st}");
    }
}