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

nordisk/facett

facett-map/tests/robot_callchain.rs

//! **Robot / headless render-confinement proof** for `facett_map::MapView` (the
//! input→PIXEL tail of the call chain that no return value can expose).
//!
//! Drives a populated `MapView` through `egui_kittest`'s real frame loop (the
//! same backend `render_proof.rs` uses), then asserts the painted output:
//!   1. the pane is a queryable AccessKit Image node carrying the layer counts
//!      (robot-driver read-back — "see what the user sees" as data), and
//!   2. the render is **non-blank** AND **confined** — measured content sits
//!      inside the allocated widget rect, nothing paints outside it.
//!
//! Run (CPU a11y leg): `cargo test -p facett-map --test robot_callchain`
//! Pixel legs need the wgpu backend (lavapipe/GPU).

use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};

use facett_core::Facet;
use facett_map::{MapView, Way};
/// **The shared one-device-at-a-time lock** — `common/one_gpu.rs`. `egui_kittest::Harness`
/// brings up its own wgpu device, libtest runs `#[test]` bodies on parallel threads, and
/// two device bring-ups at once take this binary out with a hard `signal: 11` — not a test
/// failure, a SIGSEGV whose output looks nothing like the assertion that was running.
/// MEASURED 2026-08-27 on oden by taking the lock back OUT of the two files that had it:
/// `rail_tier_pixels` 2/5 runs and `place_label_tier_pixels` 3/5 runs died that way. This
/// file rendered 0/8 without it, which is a LOWER rate and not a safe one — the hazard is
/// the concurrent bring-up, and every file that does one takes the same turn.
#[path = "common/one_gpu.rs"]
mod one_gpu;

/// Build a real multi-feature basemap (a filled water lake + a park + roads) over
/// a Liechtenstein-ish span, plus a point cluster — the shape a GeoParquet load
/// hands the view.
fn populated_map() -> MapView {
    let th = facett_core::Theme::default();
    let tag = |k: &str, v: &str| vec![(k.to_string(), v.to_string())];
    let lake = [(9.48, 47.10), (9.58, 47.10), (9.58, 47.22), (9.48, 47.22), (9.48, 47.10)];
    let park = [(9.50, 47.12), (9.54, 47.12), (9.54, 47.16), (9.50, 47.16), (9.50, 47.12)];
    let mut ways: Vec<Way> = vec![
        Way::from_lonlat(1, &lake, &tag("natural", "water"), &th).unwrap(),
        Way::from_lonlat(2, &park, &tag("leisure", "park"), &th).unwrap(),
    ];
    for i in 0..6 {
        let y = 47.11 + i as f64 * 0.02;
        ways.push(Way::from_lonlat(10 + i, &[(9.47, y), (9.60, y + 0.04)], &tag("highway", "primary"), &th).unwrap());
    }
    let pts: Vec<(f64, f64)> = (0..500).map(|i| (9.48 + (i % 25) as f64 * 0.005, 47.11 + (i / 25) as f64 * 0.005)).collect();
    MapView::new("robot-map", pts).with_ways(ways)
}

/// **Robot read-back (CPU, always runs).** The whole pane is a value-bearing
/// AccessKit Image node a robot driver / screen reader can query by label —
/// proving the rendered surface is observable as data.
#[test]
fn map_pane_is_robot_queryable_with_layer_counts() {
    let mut view = populated_map();
    let np = view.points.len();
    let nw = view.ways.len();
    let _gpu = one_gpu::hold();
    let mut harness = Harness::builder().with_size(egui::vec2(700.0, 520.0)).build_ui(move |ui| view.ui(ui));
    harness.run();

    let label = format!("robot-map — {np} points · {nw} ways");
    let pane = harness.get_by_label(&label).accesskit_node();
    assert_eq!(pane.role(), egui::accesskit::Role::Image, "pane is an Image node");
    assert_eq!(pane.numeric_value(), Some(np as f64), "point count rides the tree");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.robot",
        "pane_node_queryable",
        pane.role() == egui::accesskit::Role::Image && pane.numeric_value() == Some(np as f64),
        &format!("Image node, {np} points / {nw} ways"),
    );
}

/// **Render confinement (wgpu).** The populated map must paint a non-blank
/// framebuffer whose drawn content is CONFINED to the widget rect — no pixels
/// painted in a thin outer frame (the "lines springing outside the widget" bug).
/// Skips gracefully when no GPU/lavapipe is present.
#[test]
fn map_renders_non_blank_and_confined() {
    let _gpu = one_gpu::hold();
    let mut harness = Harness::builder().with_size(egui::vec2(640.0, 480.0)).build_ui(move |ui| {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
        // Allocate the map into a FIXED sub-rect that occupies the TOP HALF of the
        // pane, leaving the bottom half as untouched window background. Anything
        // the map paints into that bottom margin is a confinement leak.
        let full = ui.available_rect_before_wrap();
        let map_rect = egui::Rect::from_min_size(full.min, egui::vec2(full.width(), full.height() * 0.5));
        let mut view = populated_map();
        ui.scope_builder(egui::UiBuilder::new().max_rect(map_rect), |ui| {
            view.ui(ui);
        });
    });
    harness.run_steps(3);
    let img = match harness.render() {
        Ok(img) => img,
        Err(e) => {
            eprintln!("[robot] no wgpu backend ({e}) — confinement pixel leg skipped");
            return;
        }
    };

    let (w, h) = (img.width(), img.height());
    // Window-background colour: sample the bottom margin (well below the map's
    // top-half rect), which the map must never paint into.
    let win_bg = *img.get_pixel(w / 2, h - 3);
    let (wr, wg, wb) = (win_bg.0[0] as i32, win_bg.0[1] as i32, win_bg.0[2] as i32);
    let far_from = |p: &image::Rgba<u8>| {
        (p.0[0] as i32 - wr).abs() + (p.0[1] as i32 - wg).abs() + (p.0[2] as i32 - wb).abs() > 24
    };

    // Top half = the map's allocated rect (must be ~all painted: land paper +
    // features). Bottom 40% = the untouched margin (must be ~all window-bg). A
    // pixel painted there is the "paints outside the widget" leak.
    let map_h = (h as f32 * 0.5) as u32;
    let margin_y0 = (h as f32 * 0.6) as u32; // strictly below the map rect
    let mut body_content = 0usize;
    let mut body_total = 0usize;
    let mut margin_painted = 0usize;
    let mut margin_total = 0usize;
    for (_x, y, p) in img.enumerate_pixels() {
        if y < map_h {
            body_total += 1;
            if far_from(p) {
                body_content += 1;
            }
        } else if y >= margin_y0 {
            margin_total += 1;
            if far_from(p) {
                margin_painted += 1;
            }
        }
    }
    let body_ratio = body_content as f64 / body_total.max(1) as f64;
    let margin_leak = margin_painted as f64 / margin_total.max(1) as f64;
    eprintln!("[robot] map-rect content ratio {body_ratio:.4}, outside-widget leak {margin_leak:.4}");
    // Non-blank: the map's rect is overwhelmingly painted.
    assert!(body_ratio > 0.5, "the map rect is painted (ratio {body_ratio:.4})");
    // Confinement: essentially nothing painted in the margin below the widget.
    assert!(margin_leak < 0.02, "map content confined to its rect (leak {margin_leak:.4})");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.robot_confined",
        "non_blank_and_confined",
        body_ratio > 0.5 && margin_leak < 0.02,
        &format!("map_rect_ratio={body_ratio:.4} outside_leak={margin_leak:.4}"),
    );
}