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

nordisk/dwarves

dwarves-gui/tests/robot_code_map.rs

//! **ROBOT-UI: the 🏛 code room is a map you can zoom, fly and read.**
//!
//! The tab over a canned `Viz.CodeMap` (one repo, a dependency, three function cards),
//! driven through egui's real input — the same events a mouse makes:
//!
//! 1. the mouse WHEEL over the canvas changes the camera's distance (zoom to the
//!    cursor), and the canvas consumes it;
//! 2. CLICKING a head in 🎯 Focus selects it and the camera flies to it;
//! 3. RESTING the pointer on a head unfolds its source (its first line shown), and the
//!    wheel over the panel scrolls it;
//! 4. Esc closes it.
//!
//! A live render (`live_code_map_render`, ignored) draws the map of a running server
//! read-only and writes a PNG.

#![cfg(not(target_arch = "wasm32"))]

use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};

use dwarves_gui::coderoom3d::{testing::map_room, CodeRoom3DTab, CARDS_DIST};
use egui::{Event, Modifiers, MouseWheelUnit, PointerButton, Pos2, TouchPhase};
use egui_kittest::Harness;
use facett_core::Facet;
use serde_json::{json, Value};

fn wheel(h: &Harness<'_>, at: Pos2, lines: f32) {
    h.event(Event::PointerMoved(at));
    h.event(Event::MouseWheel { unit: MouseWheelUnit::Line, delta: egui::vec2(0.0, lines), phase: TouchPhase::Move, modifiers: Modifiers::NONE });
}

fn click(h: &Harness<'_>, at: Pos2) {
    h.event(Event::PointerMoved(at));
    h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
    h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
}

fn at(v: &Value) -> Pos2 {
    Pos2::new(v["at"][0].as_f64().unwrap() as f32, v["at"][1].as_f64().unwrap() as f32)
}

/// Step until `done` (a worker answers off-thread), at most a few seconds.
fn settle(h: &mut Harness<'_>, tab: &Rc<RefCell<CodeRoom3DTab>>, done: impl Fn(&Value) -> bool) -> Value {
    let t0 = Instant::now();
    loop {
        h.step();
        let s = tab.borrow().state_json();
        if done(&s) || t0.elapsed() > Duration::from_secs(10) {
            return s;
        }
        std::thread::sleep(Duration::from_millis(10));
    }
}

#[test]
fn the_code_map_zooms_on_the_wheel_flies_to_a_clicked_head_and_unfolds_its_source() {
    let tab = Rc::new(RefCell::new(CodeRoom3DTab::with_source(map_room(), "w")));
    tab.borrow_mut().update_json(r#"{"wait":true}"#);
    let t = tab.clone();
    let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui(move |ui| t.borrow_mut().ui(ui));
    let s = settle(&mut h, &tab, |s| s["canvas"].is_array());
    assert_eq!(s["from_room"], true, "{s}");
    let canvas = s["canvas"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap() as f32).collect::<Vec<_>>();
    let mid = Pos2::new((canvas[0] + canvas[2]) * 0.5, (canvas[1] + canvas[3]) * 0.5);

    // 1. the wheel zooms: the camera comes closer, and the canvas took the event
    let d0 = s["camera"]["dist"].as_f64().unwrap();
    wheel(&h, mid, 3.0);
    let s = settle(&mut h, &tab, |s| s["wheel_events"].as_u64() > Some(0));
    let d1 = s["camera"]["dist"].as_f64().unwrap();
    assert!(d1 < d0 * 0.8, "the wheel zooms in: {d0} → {d1}");
    wheel(&h, mid, -3.0);
    let s = settle(&mut h, &tab, |s| s["wheel_events"].as_u64() > Some(1));
    assert!(s["camera"]["dist"].as_f64().unwrap() > d1 * 1.2, "and back out");

    // fly in over the cards: the tile is asked and its heads are drawn
    tab.borrow_mut().update_json(&json!({ "camera": { "target": [20.0, 0.0, 32.0], "dist": 40.0, "yaw": 0.0, "pitch": 1.0 }, "nav": "focus" }).to_string());
    let s = settle(&mut h, &tab, |s| s["heads_screen"].as_array().is_some_and(|a| a.len() == 3));
    assert_eq!(s["lod"], "cards", "{s}");
    assert!((s["camera"]["dist"].as_f64().unwrap() as f32) < CARDS_DIST);
    assert_eq!(s["splines_drawn"], 2, "{s}");

    // 2. a click on `total`'s head in 🎯 Focus: selected, and the camera flies to it
    let total = s["heads_screen"].as_array().unwrap().iter().find(|x| x["card"] == 1).unwrap().clone();
    click(&h, at(&total));
    let s = settle(&mut h, &tab, |s| s["selected"] == 1 && s["camera"]["flying"] == false && s["camera"]["target"][0].as_f64().unwrap() > 25.0);
    assert_eq!(s["selected"], 1, "{s}");
    let tx = s["camera"]["target"][0].as_f64().unwrap();
    assert!((tx - 26.5).abs() < 0.5, "the camera landed on total's card (x 26.5): {tx}");
    // the click pinned total's source open; Esc closes it
    h.key_press(egui::Key::Escape);
    let s = settle(&mut h, &tab, |s| s["panels"].as_array().is_some_and(Vec::is_empty));
    assert!(s["panels"].as_array().unwrap().is_empty(), "Esc closes the source: {s}");

    // 3. resting on `checkout`'s head unfolds its source; the wheel over it scrolls it
    tab.borrow_mut().update_json(&json!({ "camera": { "target": [24.0, 0.0, 34.0], "dist": 55.0, "yaw": 0.0, "pitch": 1.0 }, "nav": "stay" }).to_string());
    let s = settle(&mut h, &tab, |s| s["heads_screen"].as_array().is_some_and(|a| a.iter().any(|x| x["card"] == 0)));
    let checkout = at(s["heads_screen"].as_array().unwrap().iter().find(|x| x["card"] == 0).unwrap());
    h.event(Event::PointerMoved(checkout));
    let s = settle(&mut h, &tab, |s| s["panels"][0]["first_line"].is_string());
    assert_eq!(s["hovered"], 0, "{s}");
    assert_eq!(s["panels"][0]["first_line"], "pub fn checkout(cart: &Cart) -> u64 {", "{s}");
    assert_eq!(s["panels"][0]["pinned"], false, "a resting pointer opens it, unpinned");
    let panel = at(&s["panels_screen"][0]);
    wheel(&h, panel, -2.0);
    let s = settle(&mut h, &tab, |s| s["panels"][0]["scroll"].as_f64() > Some(0.0));
    assert!(s["panels"][0]["scroll"].as_f64().unwrap() > 0.0, "the wheel over the source scrolls it: {s}");
    assert_ne!(s["panels"][0]["first_line"], "pub fn checkout(cart: &Cart) -> u64 {");
    assert!((s["camera"]["dist"].as_f64().unwrap() - 55.0).abs() < 1e-3, "and does not zoom the map");
    // 4. a click on the panel pins it
    click(&h, panel);
    let s = settle(&mut h, &tab, |s| s["panels"][0]["pinned"] == true);
    assert_eq!(s["panels"][0]["pinned"], true, "{s}");

    if let Ok(png) = std::env::var("DWARVES_CODE_MAP_PNG") {
        match h.render() {
            Ok(img) => {
                img.save(&png).expect("write png");
                println!("wrote {png}");
            }
            Err(e) => println!("no render: {e}"),
        }
    }
}

/// The live render: `DWARVES_LIVE_FORGE=http://10.125.73.187:7900 DWARVES_LIVE_WS=nordisk
/// DWARVES_CODE_MAP_PNG=/tmp/code-map.png [DWARVES_CODE_MAP_FIND=nornir]
/// cargo test -p dwarves-gui --test robot_code_map live_code_map_render -- --ignored --nocapture`.
/// Read-only: it asks and looks, it clicks nothing.
#[test]
#[ignore]
fn live_code_map_render() {
    let Ok(live) = std::env::var("DWARVES_LIVE_FORGE") else {
        eprintln!("DWARVES_LIVE_FORGE not set — nothing to render");
        return;
    };
    let ws = std::env::var("DWARVES_LIVE_WS").unwrap_or_else(|_| "nordisk".into());
    let token = std::env::var("DWARVES_LIVE_TOKEN").unwrap_or_default();
    let tab = Rc::new(RefCell::new(CodeRoom3DTab::new(live, token, ws)));
    let t = tab.clone();
    let mut h = Harness::builder().with_size(egui::vec2(1800.0, 1200.0)).build_ui(move |ui| t.borrow_mut().ui(ui));
    let t0 = Instant::now();
    let deadline = Duration::from_secs(std::env::var("DWARVES_CODE_MAP_WAIT_S").ok().and_then(|s| s.parse().ok()).unwrap_or(240));
    while t0.elapsed() < deadline {
        h.step();
        let s = tab.borrow().state_json();
        if s["from_room"] == true || (s["pending"] == false && s["queued"].as_array().is_some_and(Vec::is_empty) && s["answer"]["state"] != "pending") {
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }
    if let Ok(cam) = std::env::var("DWARVES_CODE_MAP_CAMERA") {
        tab.borrow_mut().update_json(&json!({ "camera": serde_json::from_str::<Value>(&cam).unwrap() }).to_string());
    }
    if let Ok(q) = std::env::var("DWARVES_CODE_MAP_FIND") {
        tab.borrow_mut().update_json(&json!({ "find": q }).to_string());
        tab.borrow_mut().update_json(r#"{"wait":true}"#);
    }
    // DWARVES_CODE_MAP_GO=1: stand over the first function found, close enough for its
    // card, and rest the pointer on its head (its source unfolds)
    let go = std::env::var("DWARVES_CODE_MAP_GO").is_ok();
    let mut target_card: Option<u64> = None;
    if go {
        let s = tab.borrow().state_json();
        if let Some(h) = s["hits_list"].as_array().and_then(|a| a.iter().find(|h| h["what"] == "fn")) {
            let r: Vec<f64> = h["rect"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap()).collect();
            let dist: f64 = std::env::var("DWARVES_CODE_MAP_DIST").ok().and_then(|d| d.parse().ok()).unwrap_or(45.0);
            target_card = h["card"].as_u64();
            tab.borrow_mut().update_json(&json!({ "camera": { "target": [(r[0] + r[2]) * 0.5 + 8.0, 0.0, (r[1] + r[3]) * 0.5 + 4.0], "dist": dist, "yaw": 0.0, "pitch": 1.0 } }).to_string());
        }
    }
    // let the tiles the camera sees land
    let t1 = Instant::now();
    while t1.elapsed() < deadline {
        h.step();
        let s = tab.borrow().state_json();
        if s["pending"] == false && s["queued"].as_array().is_some_and(Vec::is_empty) {
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }
    for _ in 0..5 {
        h.step();
    }
    if let Some(card) = target_card {
        let s = tab.borrow().state_json();
        if let Some(head) = s["heads_screen"].as_array().and_then(|a| a.iter().find(|x| x["card"] == card)) {
            h.event(Event::PointerMoved(at(head)));
            let t2 = Instant::now();
            while t2.elapsed() < Duration::from_secs(60) {
                h.step();
                if tab.borrow().state_json()["panels"][0]["first_line"].is_string() {
                    break;
                }
                std::thread::sleep(Duration::from_millis(50));
            }
            for _ in 0..5 {
                h.step();
            }
        }
    }
    let s = tab.borrow().state_json();
    let mut short = s.clone();
    short["hits_list"] = json!(s["hits_list"].as_array().map(Vec::len));
    short["heads_screen"] = json!(s["heads_screen"].as_array().map(Vec::len));
    short["plates_screen"] = json!(s["plates_screen"].as_array().map(Vec::len));
    println!("{}", serde_json::to_string_pretty(&short).unwrap());
    if let Ok(png) = std::env::var("DWARVES_CODE_MAP_PNG") {
        match h.render() {
            Ok(img) => {
                img.save(&png).expect("write png");
                println!("wrote {png}");
            }
            Err(e) => println!("no render: {e}"),
        }
    }
}