facett-jobview/tests/step_bar_png.rs
//! **Render the step bar to a PNG a human can open.**
//!
//! The headless assertions prove the numbers; this proves the pixels. It renders the real
//! `JobList` through the real wgpu path and writes the frame to `$FACETT_STEPBAR_PNG`, so
//! the claim "you can see which step is green" can be checked by looking rather than by
//! trusting a test name.
//!
//! Deliberately a **partial** run: a bar standing at full proves nothing, which was the
//! whole point of the red requirement.
use facett_jobview::{JobEntry, JobList, JobStatus, StepCell, StepMeter, StepPhase};
fn meter(run: u64, caption: &str, hover: &str, steps: Vec<StepCell>) -> StepMeter {
StepMeter { generation: run, caption: caption.into(), hover: hover.into(), steps }
}
#[test]
fn write_step_bar_png() {
// **Never skip.** This used to be
// `let Ok(path) = std::env::var("FACETT_STEPBAR_PNG") else { .. return };`
// and `FACETT_STEPBAR_PNG` is set NOWHERE in the repo, so `cargo test -p
// facett-jobview` reached line 21 and reported GREEN without an egui harness, a
// wgpu device, a PNG or a single colour checked. The test EXISTED and it PASSED,
// and both of those facts were about the environment variable rather than about
// the step bar. The path now defaults into `CARGO_TARGET_TMPDIR`, so the picture is
// always rendered and the env var only chooses WHERE a human can go look at it.
let path = std::env::var("FACETT_STEPBAR_PNG")
.unwrap_or_else(|_| format!("{}/step_bar.png", env!("CARGO_TARGET_TMPDIR")));
// Row 1 — Sweden mid-run: a·b done, c ACTIVE and unmeasured, d pending.
let sweden = meter(
1,
"run #1 · step 3 of 4 · relation clip + geometry assembly",
"run #1 started because: not provisioned yet",
vec![
StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Done),
StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Done),
StepCell::new("c", "relation clip + geometry assembly", StepPhase::Active),
StepCell::new("d", "tile pyramid", StepPhase::Pending),
],
);
// Row 2 — the dwarves ORDER FAULT: step b red because it overtook step a.
let norway = meter(
1,
"run #1 · step 2 of 4 FAILED · convert pass 2 (nodes/ways/relations)",
"OUT OF ORDER: step b started while step a is still ACTIVE",
vec![
StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Active),
StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Failed),
StepCell::new("c", "relation clip + geometry assembly", StepPhase::Pending),
StepCell::new("d", "tile pyramid", StepPhase::Pending),
],
);
// Row 3 — dwarves populate with a MEASURED count: clone is 12 of 19 members.
let populate = meter(
4,
"run #4 · step 1 of 4 · clone 19 workspace members",
"run #4 started because: populate requested",
vec![
StepCell::new("1", "clone 19 workspace members", StepPhase::Active).with_count(12, 19),
StepCell::new("2", "build the warehouse", StepPhase::Pending),
StepCell::new("3", "scan lenses", StepPhase::Pending),
StepCell::new("4", "index vectors", StepPhase::Pending),
],
);
// Row 4 — a finished run, so full is visibly DIFFERENT from the three above.
let done = meter(
2,
"run #2 · 4 of 4 steps complete",
"run #2 started because: server start",
vec![
StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Done),
StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Done),
StepCell::new("c", "relation clip + geometry assembly", StepPhase::Done),
StepCell::new("d", "tile pyramid", StepPhase::Done),
],
);
let mk = |id: &str, target: &str, st: JobStatus, m: &StepMeter| {
let mut e = JobEntry::new(id, "collector", st);
e.target = target.into();
e.with_steps(m)
};
let mut view = JobList::new("Jobs — collectors");
view.set_jobs(vec![
mk("map-regions/sweden", "Sweden", JobStatus::Running, &sweden),
mk("map-regions/norway", "Norway", JobStatus::Failed, &norway),
mk("populate/workspace_nordisk", "workspace_nordisk", JobStatus::Running, &populate),
mk("map-regions/denmark", "Denmark", JobStatus::Done, &done),
]);
// The theme the pane actually painted with, captured from inside the harness — so
// the assertions below read `StepPhase::color`, the ONE writer of the step palette,
// instead of a second copy of four hex literals free to drift from it.
let seen_theme: std::rc::Rc<std::cell::RefCell<Option<facett_core::Theme>>> = Default::default();
let th_sink = seen_theme.clone();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(760.0, 240.0))
.build_ui(move |ui| {
*th_sink.borrow_mut() = Some(facett_core::theme::theme(ui));
facett_core::Facet::ui(&mut view, ui);
});
h.run_steps(3); // the running rows animate, so step a fixed number of frames
let img = h.render().expect("wgpu render");
img.save(&path).expect("write png");
eprintln!("step-bar picture written: {path} ({}x{})", img.width(), img.height());
let th = (*seen_theme.borrow()).expect("the pane painted at least one frame");
let thin = thin_phases(img.as_raw(), img.width(), img.height(), &th);
assert!(
thin.is_empty(),
"the step bar did not paint its own palette — under {MIN_PHASE_PX} px each for: {}",
thin.join(", ")
);
}
/// Per-channel tolerance when matching a painted pixel to a phase colour. The cells are
/// flat `rect_filled`s, so their interiors land on the colour; the slack is for the
/// sRGB/linear round trip through the framebuffer, not for a different colour.
const COLOUR_TOL: u32 = 6;
/// Pixels one phase colour must cover to count as painted.
///
/// MEASURED on the live fixture: `pending` 1165, `done` 559, `active` 340, and `failed`
/// — which appears in exactly ONE cell — 93. On the all-identical probe frame the three
/// missing phases read 4 / 36 / 7. So a painted phase and a missing one are an order of
/// magnitude apart and this floor sits between them: a guard against a phase VANISHING,
/// not a golden of the layout.
const MIN_PHASE_PX: usize = 60;
/// **The step bar's OWN palette, phase by phase** — which phases failed to paint.
///
/// `assert!(distinct.len() > 8)`, the check that stood here, is cleared by panel chrome,
/// a border and one line of antialiased heading text on any 760x240 egui frame with all
/// four `StepCell`s missing; it can only see a single-colour frame. This asks the
/// question the picture was written to answer, and asks it through `StepPhase::color` —
/// the ONE writer of the step palette — rather than through a second copy of four hex
/// literals free to drift from it.
fn thin_phases(px: &[u8], w: u32, h: u32, th: &facett_core::Theme) -> Vec<String> {
let count_near = |c: egui::Color32| {
px.chunks(4)
.filter(|p| {
let d = |a: u8, b: u8| i32::from(a).abs_diff(i32::from(b));
d(p[0], c.r()).max(d(p[1], c.g())).max(d(p[2], c.b())) <= COLOUR_TOL
})
.count()
};
let distinct = px.chunks(4).map(|c| (c[0], c[1], c[2])).collect::<std::collections::HashSet<_>>();
println!(" frame {w}x{h} {} distinct colours", distinct.len());
let mut thin = Vec::new();
for phase in [StepPhase::Done, StepPhase::Active, StepPhase::Pending, StepPhase::Failed] {
let c = phase.color(th);
let n = count_near(c);
println!(" {:<8} rgb({:>3},{:>3},{:>3}) {n:>7} px", phase.key(), c.r(), c.g(), c.b());
if n < MIN_PHASE_PX {
thin.push(format!("{} ({} px of rgb({},{},{}))", phase.key(), n, c.r(), c.g(), c.b()));
}
}
thin
}
/// **THE RED for the guard above.** A bar whose four cells are all `Pending` — the
/// "four identical grey cells" a broken phase mapping produces — must FAIL the palette
/// check, and must fail it for `done`, `active` and `failed` by name. Same harness, same
/// renderer, same measurement; only the data changes.
///
/// It also re-runs the check this replaces on that same frame, so the reason the old one
/// could not go red is asserted rather than asserted about.
#[test]
fn a_bar_of_four_identical_cells_fails_the_palette_check() {
let flat = meter(
1,
"run #1 · every cell the same phase",
"the shape of a broken phase mapping",
vec![
StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Pending),
StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Pending),
StepCell::new("c", "relation clip + geometry assembly", StepPhase::Pending),
StepCell::new("d", "tile pyramid", StepPhase::Pending),
],
);
// `Queued`, not `Running`: the row STATUS dot is painted in the same amber
// `StepPhase::Active` uses, and a Running row therefore puts ~66 px of active-amber
// on the frame with no active cell in it. The probe must isolate the step bar.
let mut e = JobEntry::new("map-regions/sweden", "collector", JobStatus::Queued);
e.target = "Sweden".into();
let mut view = JobList::new("Jobs — collectors");
view.set_jobs(vec![e.with_steps(&flat)]);
let seen_theme: std::rc::Rc<std::cell::RefCell<Option<facett_core::Theme>>> = Default::default();
let th_sink = seen_theme.clone();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(760.0, 240.0))
.build_ui(move |ui| {
*th_sink.borrow_mut() = Some(facett_core::theme::theme(ui));
facett_core::Facet::ui(&mut view, ui);
});
h.run_steps(3);
let img = h.render().expect("wgpu render");
let th = (*seen_theme.borrow()).expect("the pane painted at least one frame");
let thin = thin_phases(img.as_raw(), img.width(), img.height(), &th);
for want in ["done", "active", "failed"] {
assert!(
thin.iter().any(|t| t.starts_with(want)),
"a bar of four identical cells must report `{want}` as unpainted, got {thin:?}"
);
}
// …and the check this file used to carry passes that very frame:
let distinct = img
.as_raw()
.chunks(4)
.map(|c| (c[0], c[1], c[2]))
.collect::<std::collections::HashSet<_>>();
assert!(
distinct.len() > 8,
"the old `distinct.len() > 8` cannot tell a bar of four identical cells from a live one \
({} distinct colours in a frame with three phases missing)",
distinct.len()
);
}