facett-pipeline/tests/flow_lane.rs
use egui::{Rect, pos2, vec2};
use facett_core::Facet;
use facett_pipeline::{PipelineNode, PipelineView, Status};
#[test]
fn six_column_layout_keeps_public_pitch_and_state_shape() {
let view = PipelineView::new(
"chain",
(0..6).map(|i| (format!("n{i}"), Status::Idle)).collect(),
(1..6).map(|i| (i, i - 1)).collect(),
);
assert_eq!(
view.layout_scene_centers(),
(0..6)
.map(|i| pos2(110.0 + i as f32 * 220.0, 68.0))
.collect::<Vec<_>>()
);
assert_eq!(
view.state_json()["dag"],
serde_json::json!({"nodes":6,"edges":5,"columns":6,"selected":null})
);
assert_eq!(view.flow_scene().graph().edges[0].from, "p0");
assert_eq!(view.flow_scene().graph().edges[0].to, "p1");
}
#[test]
fn reconstruction_and_camera_do_not_change_geometry_revision() {
let make = || {
PipelineView::from_nodes(
"p",
vec![
PipelineNode::new("source", Status::Done),
PipelineNode::new("sink", Status::Running).with_throughput_rows_s(0.0),
],
vec![(1, 0)],
)
};
let mut a = make();
let b = make();
let revision = a.flow_scene().generation();
assert_eq!(revision, b.flow_scene().generation());
a.fit_to(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 400.0)));
a.pan_pixels(10.0, 20.0);
a.set_clock(27.0);
assert_eq!(revision, a.flow_scene().generation());
}
#[test]
fn invalid_public_edges_remain_in_state_but_do_not_enter_flow() {
let view = PipelineView::new("p", vec![("n".into(), Status::Idle)], vec![(8, 0)]);
assert_eq!(view.state_json()["dag"]["edges"], 1);
assert!(view.flow_scene().graph().edges.is_empty());
}
#[test]
fn statuses_and_consumer_rates_keep_the_public_node_order() {
use facett_flow::attr::Status as FlowStatus;
let view = PipelineView::from_nodes(
"p",
vec![
PipelineNode::new("source", Status::Done),
PipelineNode::new("sink", Status::Failed).with_throughput_rows_s(123.0),
PipelineNode::new(
"unobserved",
Status::Unknown(facett_pipeline::UnknownReason::Stale),
),
PipelineNode::new("running", Status::Running),
],
vec![(1, 0), (2, 1), (3, 2)],
);
let scene = view.flow_scene();
assert_eq!(
scene.status().unwrap().nodes,
vec![
Some(FlowStatus::Ok),
Some(FlowStatus::Error),
Some(FlowStatus::Slow),
None,
]
);
assert_eq!(scene.rates().unwrap().edges, vec![Some(123.0), None, None]);
assert_eq!(scene.edge_point(0, 0.0).unwrap(), pos2(190.0, 68.0));
assert_eq!(scene.edge_point(0, 1.0).unwrap(), pos2(250.0, 68.0));
}
fn cpu_frame(clock: f64, rate: Option<f32>) -> String {
let source = PipelineNode::new("source", Status::Idle);
let mut sink = PipelineNode::new("sink", Status::Idle);
sink.throughput_rows_s = rate;
let mut view =
PipelineView::from_nodes("p", vec![source, sink], vec![(1, 0)]).with_clock(clock);
let ctx = egui::Context::default();
let mut shapes = String::new();
for _ in 0..3 {
let mut output = ctx.run_ui(
egui::RawInput {
screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(900.0, 500.0))),
..Default::default()
},
|ui| {
egui::CentralPanel::default().show(ui, |ui| view.ui(ui));
},
);
shapes = format!("{:?}", output.shapes);
output.textures_delta.clear();
}
shapes
}
#[test]
fn cpu_packets_obey_injected_clock_and_zero_rate() {
let first = cpu_frame(0.0, None);
assert_eq!(first, cpu_frame(0.0, None));
assert_ne!(
first,
cpu_frame(1.0, None),
"unspecified throughput preserves flow packets"
);
assert_eq!(
cpu_frame(0.0, Some(0.0)),
cpu_frame(1.0, Some(0.0)),
"dead edge has no moving packets"
);
}
#[test]
fn accessibility_budget_does_not_remove_visible_geometry() {
use egui_kittest::kittest::{NodeT, Queryable};
let mut view = PipelineView::new(
"many stages",
(0..2_050)
.map(|i| (format!("stage{i}"), Status::Idle))
.collect(),
Vec::new(),
)
.with_animation(false);
// Stack all 2,050 rows inside the viewport at a deliberately tiny overview scale.
view.set_camera(
facett_core::nav::Navigable {
scale: 0.002,
offset: [100.0, 100.0],
min_scale: 0.001,
..Default::default()
},
true,
);
assert_eq!(
view.visible_node_count(Rect::from_min_size(pos2(0.0, 0.0), vec2(900.0, 600.0))),
2_050
);
let mut harness = egui_kittest::Harness::builder()
.with_size(vec2(900.0, 600.0))
.build_ui(move |ui| view.ui(ui));
harness.run();
let stages = harness
.get_all_by_role(egui::accesskit::Role::Button)
.filter(|n| {
n.accesskit_node()
.label()
.is_some_and(|l| l.starts_with("stage"))
})
.count();
assert_eq!(stages, 2_000);
}