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}"),
);
}