Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

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"
        );
    }
}