crates/knut-pipelines/src/testkit.rs
//! **Shared test fixtures for the `ViewerState` model** — the synthetic runs a
//! consumer injects to exercise its rendering of a pipeline.
//!
//! ## Why this exists
//!
//! `ViewerState`/`ExecutionProgress` are knut's types, but knut is not their only
//! test consumer: korp renders the same model through facett, and facett's own
//! connector matrix crosses it too. Each of them needs to inject a *realistic*
//! progress snapshot, and each had hand-built the same one — knut's
//! `tests/facett_handoff_contract.rs` and korp's `src/main.rs` test module
//! carried byte-identical `apply_prog` bodies.
//!
//! That is a live hazard, not a tidiness complaint: these fixtures are struct
//! literals over knut's PUBLIC types. The moment [`StageProgress`] or
//! [`ExecutionProgress`] gains a field, every hand-rolled copy in every repo
//! stops compiling independently, and each has to be found and fixed by hand.
//! Owning the fixture here means the type's owner updates it once and every
//! consumer's matrix keeps building.
//!
//! Off by default behind `testkit` — no fixture rides in a release build.
use crate::progress::{ExecutionProgress, StageProgress, ViewerState, ViewerUpdate};
/// The progress dimension of a viewer matrix: no progress reported yet, a run
/// mid-flight, or a finished run. These are the three states a pipeline view has
/// to render differently, so they are the three a matrix must cross.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Prog {
/// No `Progress` update has arrived — `ViewerState::progress` stays `None`.
None,
/// A run in flight: stage 0 complete, stage 1 partway, tasks still inflight.
Streaming,
/// A finished run: every stage done, nothing inflight.
Complete,
}
impl Prog {
/// Every variant, for a matrix to iterate.
pub const ALL: [Prog; 3] = [Prog::None, Prog::Streaming, Prog::Complete];
/// The snapshot this variant stands for, or `None` for [`Prog::None`].
///
/// Exposed separately from [`apply_prog`] so a consumer can assert against
/// the very numbers it injected instead of restating them.
pub fn snapshot(self) -> Option<ExecutionProgress> {
match self {
Prog::None => None,
Prog::Streaming => Some(ExecutionProgress {
stages: vec![
StageProgress {
stage_id: 0,
num_tasks: 4,
num_completed_tasks: 4,
input_bytes_read: 1024 * 1024,
done: true,
},
StageProgress {
stage_id: 1,
num_tasks: 4,
num_completed_tasks: 1,
input_bytes_read: 512 * 1024,
done: false,
},
],
num_inflight_tasks: 3,
}),
Prog::Complete => Some(ExecutionProgress {
stages: vec![StageProgress {
stage_id: 0,
num_tasks: 6,
num_completed_tasks: 6,
input_bytes_read: 3 * 1024 * 1024,
done: true,
}],
num_inflight_tasks: 0,
}),
}
}
}
/// Inject `prog`'s progress snapshot into `vs` through the REAL
/// [`ViewerState::apply`] path — the same call a live run makes, so a fixture can
/// never diverge from how progress actually reaches the model.
pub fn apply_prog(vs: &mut ViewerState, prog: Prog) {
if let Some(snapshot) = prog.snapshot() {
vs.apply(ViewerUpdate::Progress(snapshot));
}
}
#[cfg(test)]
mod tests {
use super::*;
/// RED WHEN BROKEN: the fixture must reach `ViewerState` through `apply`, and
/// the numbers a consumer asserts on must be the numbers that land. If
/// `apply_prog` stopped applying (or `Prog::None` started applying), or a
/// stage's counts drifted from `snapshot()`, this fails.
#[test]
fn each_prog_lands_exactly_its_snapshot_in_the_state() {
for prog in Prog::ALL {
let mut vs = ViewerState::default();
assert!(vs.progress.is_none(), "a fresh state has no progress");
apply_prog(&mut vs, prog);
assert_eq!(
vs.progress,
prog.snapshot(),
"{prog:?}: what the fixture injects is what a consumer can assert",
);
}
}
/// The three variants are genuinely three different renderings — a matrix
/// crossing them would be pointless if two collapsed onto the same state.
#[test]
fn the_three_progress_states_are_distinct() {
let streaming = Prog::Streaming.snapshot().unwrap();
let complete = Prog::Complete.snapshot().unwrap();
assert!(Prog::None.snapshot().is_none());
assert_ne!(streaming, complete);
// Streaming is mid-flight: work outstanding, tasks inflight.
assert_eq!(streaming.num_inflight_tasks, 3);
assert!(
streaming.stages.iter().any(|s| !s.done),
"a run in flight has an unfinished stage"
);
// Complete is finished: nothing inflight, every stage done.
assert_eq!(complete.num_inflight_tasks, 0);
assert!(
complete.stages.iter().all(|s| s.done),
"a finished run has no unfinished stage"
);
}
}