facett-jobview/tests/step_meter_red.rs
//! **The step meter must be able to show something other than "4 of 4".**
//!
//! A progress bar that always reads full is the hollow guard this whole seam exists to
//! avoid: it is green because it cannot be anything else, not because the work finished.
//! Every assertion below therefore pins a value the bar can only produce by actually
//! reading its input — and the partial cases come first, because a test that only ever
//! feeds a complete run proves nothing about a bar drawn mid-run.
//!
//! Nothing here needs a GPU or a window: [`step_segments`] is pure geometry and
//! `state_json` is pure data, so the drawn layout and the published text are both
//! assertable headlessly.
use facett_jobview::{
JobEntry, JobList, JobStatus, STEPS_META_KEY, StepCell, StepMeter, StepPhase, step_segments,
};
/// The four steps of korp's region chain, at an arbitrary point: a and b finished, c
/// being worked on, d not reached.
fn mid_run() -> StepMeter {
StepMeter {
generation: 3,
caption: "run #3 · step 3 of 4 · relation clip + geometry assembly".into(),
hover: "run #3 started because: a newer planet file appeared\n\
a convert pass 1 (node coord store) DONE\n\
b convert pass 2 (nodes/ways/relations) DONE\n\
c relation clip + geometry assembly ACTIVE\n\
d tile pyramid PENDING"
.into(),
steps: 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),
],
}
}
/// **The RED case.** Two of four steps done, the third active — the bar must say so.
///
/// If any of these read `4`, `1.0` or `complete: true`, the meter is not reading its
/// input and every green it ever shows is worthless.
#[test]
fn a_partial_run_reads_partial_everywhere() {
let m = mid_run();
assert_eq!(m.total(), 4, "four steps in, four steps out");
assert_eq!(m.done(), 2, "a and b are done — NOT four");
assert_eq!(m.active(), Some(2), "c (index 2) is the step being worked on");
assert_eq!(m.human_index(), 3, "the human reads 'step 3 of 4'");
assert!(!m.is_complete(), "a run with an active step is NOT complete");
assert_eq!(m.failed(), None, "nothing failed here");
// The drawn sentence, and the fact that it is NOT a full-bar sentence.
let cap = m.caption_text();
assert_eq!(cap, "run #3 · step 3 of 4 · relation clip + geometry assembly");
assert!(!cap.contains("4 of 4"), "the caption must not claim a finished run: {cap}");
// And the published data agrees, key for key.
let j = m.state_json();
assert_eq!(j["index"], 3);
assert_eq!(j["total"], 4);
assert_eq!(j["done"], 2);
assert_eq!(j["active"], 2);
assert_eq!(j["complete"], false);
assert_eq!(j["generation"], 3);
assert_eq!(
j["steps"][0]["phase"], "done",
"step a done"
);
assert_eq!(j["steps"][2]["phase"], "active", "step c active");
assert_eq!(j["steps"][3]["phase"], "pending", "step d not reached");
}
/// The **same meter with every step done** must read differently from the partial one.
/// Two states that render identically are one state, and a bar with one state is a
/// decoration.
#[test]
fn complete_and_partial_are_distinguishable() {
let partial = mid_run();
let mut full = mid_run();
for s in full.steps.iter_mut() {
s.phase = StepPhase::Done;
}
full.caption = "run #3 · 4 of 4 steps complete".into();
assert_ne!(partial.done(), full.done());
assert_ne!(partial.is_complete(), full.is_complete());
assert_ne!(partial.caption_text(), full.caption_text());
assert_ne!(partial.state_json(), full.state_json());
assert_eq!(full.human_index(), 4);
assert!(full.is_complete());
}
/// A **failed** run stops on the step that died, keeps the finished ones finished, and
/// says which one — `Failed` is not `Pending` and must not be paintable as it.
#[test]
fn a_failed_step_is_its_own_state() {
let mut m = mid_run();
m.steps[2].phase = StepPhase::Failed;
m.steps[2].line = "c relation clip + geometry assembly FAILED — no relations.parquet".into();
m.caption = "run #3 · step 3 of 4 FAILED · relation clip + geometry assembly".into();
assert_eq!(m.failed(), Some(2));
assert_eq!(m.active(), None, "a failed run has no active step");
assert_eq!(m.done(), 2, "the two steps that finished still count");
assert_eq!(m.human_index(), 3, "the human still reads step 3");
assert!(!m.is_complete());
assert!(m.caption_text().contains("FAILED"));
assert_eq!(m.state_json()["failed"], 2);
assert_eq!(m.state_json()["steps"][2]["phase"], "failed");
assert!(
m.state_json()["steps"][2]["line"]
.as_str()
.is_some_and(|l| l.contains("no relations.parquet")),
"the reason the step failed reaches the tooltip line"
);
}
/// **A restart must read as a new run, not as lost ground.**
///
/// This is the whole reason `generation` exists: the two meters below have the *same*
/// step counts in the same direction (3 → 1) and are only distinguishable by their run
/// number. Assert exactly that.
#[test]
fn a_restart_is_distinguishable_from_a_regression() {
let before = mid_run(); // run #3, step 3 of 4
// The collector found a newer planet file and began again at step 1.
let after = StepMeter {
generation: 4,
caption: "run #4 · step 1 of 4 · convert pass 1 (node coord store)".into(),
hover: "run #4 started because: a newer planet file appeared".into(),
steps: vec![
StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Active),
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),
],
};
// The bar really did go backwards …
assert!(after.human_index() < before.human_index(), "3 → 1: the count DID drop");
// … and the run number is what says that is fine.
assert!(after.generation > before.generation, "a restart bumps the run number");
assert!(
after.caption_text().contains("run #4"),
"the run number is in the sentence a human reads, not only in the data: {}",
after.caption_text()
);
assert!(
after.hover_text().contains("a newer planet file appeared"),
"and the reason it restarted is in the hover, so a reset bar does not read as a fault"
);
// Without the generation the two would be indistinguishable — prove that claim
// rather than asserting it in a comment.
let before_blind = StepMeter { generation: 0, ..before.clone() };
let after_blind = StepMeter { generation: 0, ..after.clone() };
assert!(
before_blind.human_index() > after_blind.human_index(),
"stripped of run identity, a restart looks exactly like a regression"
);
}
/// An **empty** meter is not a complete one. A zero-step bar that reports `complete` is
/// the hollow green in its purest form.
#[test]
fn an_empty_meter_is_never_complete() {
let m = StepMeter::default();
assert_eq!(m.total(), 0);
assert!(!m.is_complete(), "0 of 0 is NOT done");
assert_eq!(m.human_index(), 0);
assert_eq!(m.caption_text(), "", "no steps ⇒ no sentence, not a fabricated one");
}
/// The meter survives the `meta` round-trip a host actually uses, and a row without one
/// decodes to `None` rather than to an empty bar.
#[test]
fn the_meta_channel_round_trips_and_absence_is_none() {
let m = mid_run();
let e = JobEntry::new("sweden", "regions", JobStatus::Running).with_steps(&m);
assert_eq!(e.steps_meter().as_ref(), Some(&m), "what went in comes back out");
assert_eq!(
e.meta.iter().filter(|(k, _)| k == STEPS_META_KEY).count(),
1,
"one slot only"
);
// Re-stamping replaces rather than accumulates (a producer republishes every step).
let mut m2 = m.clone();
m2.generation = 9;
let e = e.with_steps(&m2);
assert_eq!(e.meta.iter().filter(|(k, _)| k == STEPS_META_KEY).count(), 1);
assert_eq!(e.steps_meter().unwrap().generation, 9);
// A plain row has no meter at all.
let plain = JobEntry::new("other", "ingest", JobStatus::Running);
assert!(plain.steps_meter().is_none(), "no marker ⇒ no bar, never an empty one");
// Unparseable ⇒ None, not a panic and not a 0-of-0 bar.
let mut junk = JobEntry::new("junk", "ingest", JobStatus::Running);
junk.meta.push((STEPS_META_KEY.to_string(), "{not json".to_string()));
assert!(junk.steps_meter().is_none());
}
/// **The layout is really segmented.** Four steps must produce four disjoint rectangles
/// that span the track — not one rectangle, and not four overlapping ones. This is the
/// applied-output assertion behind "you can see which step is green".
#[test]
fn four_steps_lay_out_as_four_disjoint_segments() {
let rect = egui::Rect::from_min_size(egui::pos2(10.0, 20.0), egui::vec2(76.0, 6.0));
let segs = step_segments(rect, 4, 2.0);
assert_eq!(segs.len(), 4, "four steps, four segments");
// (76 - 3*2) / 4 = 17.5
assert!((segs[0].width() - 17.5).abs() < 0.01, "segment width: {}", segs[0].width());
for w in segs.windows(2) {
assert!(
w[0].max.x <= w[1].min.x,
"segments must not overlap: {:?} then {:?}",
w[0],
w[1]
);
assert!(
(w[1].min.x - w[0].max.x - 2.0).abs() < 0.01,
"the 2 px gap between segments is what makes the step count readable"
);
}
assert!((segs[0].min.x - 10.0).abs() < 0.01, "starts at the track's left edge");
assert!((segs[3].max.x - 86.0).abs() < 0.01, "and ends at its right edge");
// A different step count really does lay out differently.
assert_eq!(step_segments(rect, 2, 2.0).len(), 2);
assert_ne!(step_segments(rect, 2, 2.0)[0].width(), segs[0].width());
assert!(step_segments(rect, 0, 2.0).is_empty(), "no steps, no segments");
}
/// The four phases must paint four **distinguishable** colours. A step bar whose "done"
/// and "pending" resolve to the same pixel is a bar nobody can read, however correct its
/// data is.
#[test]
fn every_phase_paints_a_distinct_colour() {
let th = facett_core::theme::Theme::default();
let cols = [
StepPhase::Pending.color(&th),
StepPhase::Active.color(&th),
StepPhase::Done.color(&th),
StepPhase::Failed.color(&th),
];
for i in 0..cols.len() {
for j in (i + 1)..cols.len() {
assert_ne!(cols[i], cols[j], "phases {i} and {j} paint the same colour");
}
}
}
/// **One emitter, two consumers.** The string the list publishes must be the string the
/// row draws — the same `caption_text()`/`hover_text()` call, not a re-derivation.
#[test]
fn the_list_publishes_the_drawn_sentence_verbatim() {
let m = mid_run();
let mut list = JobList::new("Jobs");
list.set_jobs(vec![
JobEntry::new("sweden", "regions", JobStatus::Running).with_steps(&m),
JobEntry::new("norway", "regions", JobStatus::Queued), // no meter at all
]);
let s = facett_core::Facet::state_json(&list);
let jobs = s["jobs"].as_array().expect("jobs array");
assert_eq!(jobs.len(), 2);
let sweden = jobs.iter().find(|j| j["id"] == "sweden").expect("sweden row");
assert_eq!(
sweden["steps"]["caption"], m.caption_text(),
"the published caption IS the drawn caption"
);
assert_eq!(sweden["steps"]["hover"], m.hover_text());
assert_eq!(sweden["steps"]["index"], 3);
assert_eq!(sweden["steps"]["total"], 4);
assert_eq!(sweden["steps"]["complete"], false);
// Every per-step tooltip line is published exactly as the segment shows it.
for (i, cell) in m.steps.iter().enumerate() {
assert_eq!(sweden["steps"]["steps"][i]["line"], cell.line);
}
let norway = jobs.iter().find(|j| j["id"] == "norway").expect("norway row");
assert!(norway["steps"].is_null(), "a row with no steps publishes null, not an empty bar");
}
/// **A step is `Determinate` only when the producer measured something.**
///
/// The owner's rule, as a guard: fine-grained where a real number exists, plain yes/no
/// otherwise — and never an invented percentage, because on screen a fabricated fraction
/// is indistinguishable from a measured one, which devalues every measured one.
#[test]
fn only_a_measured_step_reads_determinate() {
use facett_jobview::ProgressMeter;
// dwarves populate, step "clone": the warehouse's clone_events writes one row per
// member attempt, so 12 of 19 is COUNTED.
let measured = StepCell::new("clone", "clone 19 workspace members", StepPhase::Active)
.with_count(12, 19);
match measured.meter() {
ProgressMeter::Determinate(f) => {
assert!((f - 12.0 / 19.0).abs() < 1e-6, "the real ratio, got {f}");
}
other => panic!("a counted step must read determinate, got {other:?}"),
}
assert_eq!(measured.count, Some((12, 19)));
// korp's convert: osm-katana logs phase transitions and no counter korp reads. The
// step is in flight and says exactly that — it does NOT guess.
let unmeasured =
StepCell::new("c", "relation clip + geometry assembly", StepPhase::Active);
assert_eq!(unmeasured.meter(), ProgressMeter::Indeterminate);
assert_eq!(unmeasured.count, None, "no count invented");
// Pending/done steps draw no inner bar at all — the segment colour is the answer,
// and a second rendering of it could only disagree.
assert_eq!(
StepCell::new("d", "tile pyramid", StepPhase::Pending).meter(),
ProgressMeter::None
);
assert_eq!(StepCell::new("a", "pass 1", StepPhase::Done).meter(), ProgressMeter::None);
// A zero total is unmeasured, not a division by zero.
assert_eq!(
StepCell::new("x", "nothing to do", StepPhase::Active).with_count(0, 0).meter(),
ProgressMeter::Indeterminate
);
// And the distinction survives into state_json, where a robot can assert it.
let m = StepMeter::from_steps(1, vec![measured.clone(), unmeasured.clone()]);
let j = m.state_json();
assert_eq!(j["steps"][0]["meter"]["kind"], "determinate");
assert_eq!(j["steps"][0]["meter"]["current"], 12);
assert_eq!(j["steps"][0]["meter"]["total"], 19);
assert_eq!(j["steps"][1]["meter"]["kind"], "indeterminate");
assert!(
j["steps"][1]["meter"]["fraction"].is_null(),
"an unmeasured step publishes NO fraction — not 0.0, which would read as a number"
);
}