facett-plandag/tests/render.rs
//! **Render-proof for facett-plandag.** Run a real [`PlanDagView`] through
//! `egui_kittest` so the operator-DAG painter executes, then prove (a) each node
//! rode into the AccessKit tree as a queryable Button carrying its cost-heat as a
//! numeric value (FC-4), and (b) the metric bodies + cost-heat switch are observable
//! as DATA — "renders nodes with metric bodies + cost-heat as DATA".
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use facett_core::Facet;
use facett_plandag::{CostMetric, Msg, PlanDagView, PlanEdge, PlanModel, PlanNode, PlanVersion};
/// A **wide-vs-deep** operator DAG: a source `root` feeds a WIDE node (fan_out 3,
/// three dead-end leaves) and a DEEP node (fan_out 1, heading a 4-long chain). The
/// deep node stalls MORE downstream work (4) than the wide fan (3), so it — not the
/// wider node — is the "clear this next" bottleneck the view must badge.
fn wide_vs_deep_view() -> PlanDagView {
let n = |id: &str, op: &str| PlanNode::new(id, op);
let nodes = vec![
n("root", "Root"),
n("wide", "Wide"),
n("w1", "W1"),
n("w2", "W2"),
n("w3", "W3"),
n("deep", "Deep"),
n("d1", "D1"),
n("d2", "D2"),
n("d3", "D3"),
n("d4", "D4"),
];
let e = |from: usize, to: usize| PlanEdge { from, to };
let edges = vec![e(0, 1), e(0, 5), e(1, 2), e(1, 3), e(1, 4), e(5, 6), e(6, 7), e(7, 8), e(8, 9)];
PlanDagView::new("plandag-constraints", PlanModel::new(PlanVersion::new("initial", nodes, edges)))
}
#[test]
fn plandag_paints_metric_nodes_into_the_accesskit_tree() {
let view = PlanDagView::local();
let node_count = view.resolved().nodes.len();
assert!(node_count >= 4);
let mut view_for_ui = PlanDagView::local();
let mut harness = Harness::new_ui(move |ui| {
view_for_ui.ui(ui);
});
harness.run();
// The scan + the hot HashAggregate both entered the tree as labelled Button
// nodes; the node carries its cost-heat as the AccessKit numeric value.
let scan = harness.get_by_label("BatchScan · done").accesskit_node();
assert_eq!(scan.role(), Role::Button, "an operator node is a queryable Button");
let agg = harness.get_by_label("HashAggregate · done").accesskit_node();
assert_eq!(agg.role(), Role::Button);
assert!(agg.numeric_value().unwrap() >= scan.numeric_value().unwrap(), "the slower op carries the higher cost-heat value");
}
#[test]
fn cost_heat_and_metric_bodies_are_data() {
let mut view = PlanDagView::local();
// Default metric = Time → HashAggregate (280ms) is the hottest.
let st = view.state_json();
let hottest = st["nodes"].as_array().unwrap().iter().max_by(|a, b| a["heat"].as_f64().unwrap().partial_cmp(&b["heat"].as_f64().unwrap()).unwrap()).unwrap();
assert!(hottest["title"].as_str().unwrap().contains("HashAggregate"), "by Time the agg is hottest");
assert!((hottest["heat"].as_f64().unwrap() - 1.0).abs() < 1e-6);
// Every node carries a multi-line metric body (time/mem/rows) as data.
for n in st["nodes"].as_array().unwrap() {
assert!(n["metrics"]["time_ms"].is_number() && n["metrics"]["rows"].is_number());
}
// Switch the cost-heat to Rows → the BatchScan (2M rows) becomes the hottest.
view.update(Msg::SetMetric(CostMetric::Rows));
let st = view.state_json();
assert_eq!(st["metric"], "rows");
let hottest = st["nodes"].as_array().unwrap().iter().max_by(|a, b| a["heat"].as_f64().unwrap().partial_cmp(&b["heat"].as_f64().unwrap()).unwrap()).unwrap();
assert!(hottest["title"].as_str().unwrap().contains("BatchScan"), "by Rows the scan is hottest — cost-heat switch is data");
}
#[test]
fn plandag_badges_the_top_bottleneck_and_state_agrees() {
// The overlay is on by default → the "clear next" constraint (reach-ranked,
// source root skipped) is the DEEP node, and that is what `state_json` reports.
let view = wide_vs_deep_view();
assert!(view.show_constraints());
let st = view.state_json();
assert_eq!(st["top_bottleneck"], "node:deep", "the deep chain outranks the wide fan by downstream reach");
// state_json ↔ the pure ranking agree (the pixels read the same `resolved`).
assert_eq!(view.resolved().top_bottleneck().as_deref(), Some("node:deep"));
let mut view_for_ui = wide_vs_deep_view();
let mut harness = Harness::new_ui(move |ui| {
view_for_ui.ui(ui);
});
harness.run();
// RED-when-broken: the badge (its own AccessKit node) rides in on DEEP, carrying
// its downstream reach (4) as the numeric value. A fan_out-primary sort would
// badge WIDE instead → this `get_by_label` panics; and WIDE must carry no badge.
let badge = harness.get_by_label("⚑ bottleneck · Deep").accesskit_node();
assert_eq!(badge.numeric_value().unwrap(), 4.0, "the badge exposes the downstream reach");
assert!(harness.query_by_label("⚑ bottleneck · Wide").is_none(), "the wide fan is NOT the badged constraint");
// the operator node itself still entered the tree (badge is additive).
assert!(harness.query_by_label("Deep · done").is_some());
}
#[test]
fn constraint_overlay_toggles_off() {
// Turning the overlay off drops the badge from the render (state_json still
// carries the analysis as DATA — the toggle only governs the emphasis pixels).
let mut view_for_ui = wide_vs_deep_view();
view_for_ui.update(Msg::SetConstraints(false));
let mut harness = Harness::new_ui(move |ui| {
view_for_ui.ui(ui);
});
harness.run();
assert!(harness.query_by_label("⚑ bottleneck · Deep").is_none(), "overlay off ⇒ no badge painted");
assert!(harness.query_by_label("Deep · done").is_some(), "the node itself still renders");
// the analysis is still observable as data regardless of the overlay toggle.
let mut v = wide_vs_deep_view();
v.update(Msg::SetConstraints(false));
assert_eq!(v.state_json()["top_bottleneck"], "node:deep");
assert_eq!(v.state_json()["show_constraints"], false);
}