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

nordisk/dwarves

dwarves-gui/tests/populate_steps_red.rs

//! **dwarves' populate step bar, proven RED first.**
//!
//! The flat bar this replaces was not wrong about the clone — it was wrong about the
//! other two thirds of the work, which it drew as 100 %. So the assertions that matter
//! here are the ones a bar which only tracked the clone would fail: that the run is NOT
//! complete when the members have landed, and that the phases after the clone are
//! reported as unmeasured rather than as full.
//!
//! Everything is driven through the real producer ([`observe`]) from real
//! [`PopulateProgress`] values. Nothing writes a `StepRun` into place, because a fixture
//! staged past the producer would prove only that the widget can draw a struct.

use dwarves_gui::populate_steps::{
    self, board, job_id, observe, phase_of, progress_fixture, Phase, CLONE, CODE_GRAPH, WAREHOUSE,
};
use facett_jobview::{ProgressMeter, StepPhase};
use nornir_warehouse_embed::Counter;
use step_board::OrderFault;

/// nordisk: 19 members. Every fixture below uses the real number.
const N: usize = 19;

/// A workspace id used by exactly one test.
///
/// The board is deliberately process-wide — one registry, every producer thread and
/// every pane — so two tests sharing an id are literally two producers racing on one
/// run. That is the condition `OrderFault` exists to catch, and it was catching it: the
/// first cut of this file used "nordisk" everywhere and six tests failed on each
/// other's writes. Sharing an id here would be staging the bug, not testing around it.
fn clean(ws: &str) -> String {
    board().forget(&job_id(ws));
    ws.to_string()
}

fn known(v: u64) -> Counter {
    Counter::Known { value: v }
}

// ── 1. THE PHASE DECISION — pure, and it must distinguish all three ───────────

/// **A populate is three phases and the reader must tell them apart.**
///
/// The old bar collapsed all three into `repos_done / repos_total`. If any two of these
/// come back equal, the step bar is a flat bar with extra segments.
#[test]
fn the_three_phases_are_distinguishable_from_one_poll() {
    // Members still coming in.
    let cloning = progress_fixture("nordisk", N, 12, 3, 0, Counter::Pending, Counter::Pending);
    assert_eq!(phase_of(&cloning), Phase::Clone);

    // Every member landed — the OLD bar reads 100% here and stops. The warehouse has
    // not been built and the code graph has not been consumed.
    let built = progress_fixture("nordisk", N, N, 0, 0, Counter::Pending, Counter::Pending);
    assert_eq!(
        phase_of(&built),
        Phase::Warehouse,
        "19 of 19 members cloned is the START of phase 2, not the end of the work"
    );

    // The dep graph exists (the build's product); the consumer's stamp does not.
    let scanning = progress_fixture("nordisk", N, N, 0, 0, known(1_284), Counter::Pending);
    assert_eq!(phase_of(&scanning), Phase::CodeGraph);

    // Both products present.
    let done = progress_fixture("nordisk", N, N, 0, 0, known(1_284), known(1));
    assert_eq!(phase_of(&done), Phase::Complete);

    // And a warehouse nobody is touching is not a run at all.
    let idle = progress_fixture("nordisk", 0, 0, 0, 0, Counter::Pending, Counter::Pending);
    assert_eq!(
        phase_of(&idle),
        Phase::Idle,
        "no members ⇒ no run, not 'phase 1 at 0%'"
    );

    // The four working phases are four distinct answers.
    let all = [
        phase_of(&cloning),
        phase_of(&built),
        phase_of(&scanning),
        phase_of(&done),
    ];
    for (i, a) in all.iter().enumerate() {
        for b in all.iter().skip(i + 1) {
            assert_ne!(a, b, "two phases collapsed onto one another: {a:?}");
        }
    }
}

// ── 2. THE GENUINE Determinate ────────────────────────────────────────────────

/// **The clone step shows a REAL fraction, and the phases after it show none.**
///
/// This is the whole reason dwarves wanted the bar: korp's four steps are all
/// `count: None` by design, so `Determinate` never renders there. Here the server reports
/// one row per member on every poll, and 12 of 19 must reach the segment as 12 of 19 —
/// not as `1.0`, not as `0.0`, and not as a constant that would look the same whatever
/// the server said.
#[test]
fn the_clone_step_is_genuinely_determinate_and_the_rest_are_not() {
    let ws = clean("ws-determinate");
    let p = progress_fixture(&ws, N, 12, 3, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("a fresh run is in order");

    let run = board()
        .get(&job_id(&ws))
        .expect("the producer started a run");

    // ── the MEASURED step ──
    assert_eq!(
        run.steps[CLONE].count,
        Some((12, N as u64)),
        "the count is the members, verbatim"
    );
    match run.steps[CLONE].meter() {
        ProgressMeter::Determinate(f) => {
            let want = 12.0f32 / N as f32;
            assert!((f - want).abs() < 1e-6, "12/19 and nothing else, got {f}");
            // Not the identity value at either end — a test sitting on 0.0 or 1.0 cannot
            // tell a working counter from a dead one.
            assert!(f > 0.6 && f < 0.64, "a genuine partial fill: {f}");
        }
        other => panic!("the clone step must be Determinate, got {other:?}"),
    }

    // ── and it MOVES with the server, which a constant could not ──
    let later = progress_fixture(&ws, N, 17, 2, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &later, "poll").expect("still in order");
    let run2 = board().get(&job_id(&ws)).unwrap();
    assert_eq!(run2.steps[CLONE].count, Some((17, N as u64)));
    let (f1, f2) = (
        match run.steps[CLONE].meter() {
            ProgressMeter::Determinate(f) => f,
            _ => unreachable!(),
        },
        match run2.steps[CLONE].meter() {
            ProgressMeter::Determinate(f) => f,
            _ => unreachable!(),
        },
    );
    assert!(f2 > f1, "the fraction tracks the server: {f1} → {f2}");

    // ── the UNMEASURED steps ──
    assert_eq!(
        run.steps[WAREHOUSE].count, None,
        "nobody counts the warehouse build"
    );
    assert_eq!(
        run.steps[CODE_GRAPH].count, None,
        "nobody counts the consume"
    );
}

/// **An active unmeasured step renders `Indeterminate` and publishes NO `fraction`.**
///
/// A zero in that slot reads as "0 % done" when the truth is "nobody counted", and one
/// invented percentage beside a measured one makes the measured one worthless.
#[test]
fn an_unmeasured_active_step_is_indeterminate_with_no_fraction_key() {
    let ws = clean("ws-indeterminate");
    // Every member landed → the warehouse build is ACTIVE and nothing counts it.
    let p = progress_fixture(&ws, N, N, 0, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("in order");

    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(run.steps[WAREHOUSE].phase, StepPhase::Active);
    assert_eq!(
        run.steps[WAREHOUSE].meter(),
        ProgressMeter::Indeterminate,
        "in flight and unmeasured is a POSITIVE statement, never Determinate(0.0)"
    );

    let j = run.state_json();
    let steps = j["steps"].as_array().expect("steps");
    assert_eq!(steps[WAREHOUSE]["meter"]["kind"], "indeterminate");
    assert!(
        steps[WAREHOUSE]["meter"].get("fraction").is_none(),
        "an unmeasured step must publish NO fraction key — a zero there reads as a \
         measurement. got: {}",
        steps[WAREHOUSE]["meter"]
    );
    // …while the measured one does carry its numbers.
    assert_eq!(steps[CLONE]["meter"]["kind"], "determinate");
    assert_eq!(steps[CLONE]["meter"]["current"], N);
    assert_eq!(steps[CLONE]["meter"]["total"], N);
}

/// **The run is NOT complete when the members have landed.** The exact claim the old flat
/// bar made, and the reason this work exists.
#[test]
fn a_finished_clone_is_not_a_finished_populate() {
    let ws = clean("ws-not-done");
    let p = progress_fixture(&ws, N, N, 0, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("in order");

    let run = board().get(&job_id(&ws)).unwrap();
    assert!(
        !run.is_complete(),
        "19 of 19 members is one of three phases, not the whole populate"
    );
    assert_eq!(run.done(), 1, "one phase finished, two to go");
    assert_eq!(run.human_index(), 2, "the human reads 'step 2 of 3'");

    let cap = run.caption();
    assert_eq!(cap, "run #1 · step 2 of 3 · build the warehouse");
    assert!(
        !cap.contains("3 of 3"),
        "the caption must not claim a finished populate: {cap}"
    );

    // And when the consumer DOES stamp the workspace, it finishes — otherwise this test passes for a
    // bar that can never read complete, which is the opposite hollow guard.
    let done = progress_fixture(&ws, N, N, 0, 0, known(1_284), known(1));
    observe(&ws, &done, "poll").expect("in order");
    let run = board().get(&job_id(&ws)).unwrap();
    assert!(
        run.is_complete(),
        "both products on disk ⇒ the populate really is done"
    );
    assert_eq!(run.caption(), "run #1 · 3 of 3 steps complete");
}

// ── 3. THE ORDER FAULT — two owners on one run ────────────────────────────────

/// **A second producer announcing the build over a running clone is refused, and the
/// fault reaches the caller.**
///
/// dwarves is where this stops being theoretical: the poll worker owns the clone phase
/// while a `Workspaces.Fetch` started by the button, by nornir's poll loop or by the CLI
/// owns the build.
#[test]
fn a_second_producer_overtaking_the_clone_is_an_order_fault() {
    let ws = clean("ws-fault");
    let p = progress_fixture(&ws, N, 12, 3, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("the clone phase is in order");

    // The overtaking publish, straight at the board — what a second owner does.
    let err = board()
        .enter(&job_id(&ws), WAREHOUSE)
        .expect_err("the order check MUST refuse a build announced over a running clone");

    match &err {
        OrderFault::PredecessorUnfinished {
            entered_key,
            blocker_key,
            blocker_phase,
            ..
        } => {
            assert_eq!(entered_key, "warehouse");
            assert_eq!(blocker_key, "clone");
            assert_eq!(*blocker_phase, StepPhase::Active);
        }
        other => panic!("wrong fault: {other:?}"),
    }

    // The bar is red, and the clone was NOT silently stamped Done.
    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(run.steps[WAREHOUSE].phase, StepPhase::Failed);
    assert_eq!(
        run.steps[CLONE].phase,
        StepPhase::Active,
        "the forward-fill would have hidden this"
    );
    assert!(
        run.error
            .as_deref()
            .is_some_and(|e| e.contains("OUT OF ORDER")),
        "{:?}",
        run.error
    );
}

/// **A fast tick must not walk a healthy run backwards into a red bar.**
///
/// This is the regression that nearly shipped. The pane polls every 1.5 s but only runs
/// the two table scans on every 4th tick, so on the three ticks in between `crates` and
/// `code_graph` both read `Pending` — and `phase_of`, which is pure and honest, reports
/// `Warehouse` because it can see no evidence of the build's product. Publishing that
/// over a run already on `Lens` is entering an earlier step behind a later active one:
/// `OrderFault::Backwards`, a RED bar, three ticks out of four, on a populate that is
/// perfectly healthy.
///
/// The assertion is deliberately on the DRAWN result — the step's phase and the caption —
/// and not on `observe`'s return value, because the board marks the step Failed itself.
/// A test that only checked for `Ok` would have passed through the whole bug.
#[test]
fn a_fast_tick_with_nothing_measured_does_not_walk_the_run_backwards() {
    let ws = clean("ws-fasttick");

    // A refine tick: the dep graph is on disk, so the run reaches the CODE_GRAPH phase.
    let refined = progress_fixture(&ws, N, N, 0, 0, known(1_284), Counter::Pending);
    observe(&ws, &refined, "Re-sync clicked").expect("in order");
    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(
        run.active(),
        Some(CODE_GRAPH),
        "the refine tick got us to the code-graph phase"
    );

    // Now three fast ticks. Identical server state, but the scans were skipped, so both
    // counters come back Pending — which reads as "the warehouse build has no product".
    let fast = progress_fixture(&ws, N, N, 0, 0, Counter::Pending, Counter::Pending);
    assert_eq!(
        phase_of(&fast),
        Phase::Warehouse,
        "the pure reader honestly says Warehouse — it has no evidence of anything later"
    );
    for _ in 0..3 {
        observe(&ws, &fast, "poll").expect("a fast tick is not a fault");
    }

    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(
        run.active(),
        Some(CODE_GRAPH),
        "the run held its place instead of falling back"
    );
    assert_eq!(run.failed(), None, "and NOTHING went red: {:?}", run.error);
    assert_eq!(run.steps[CODE_GRAPH].phase, StepPhase::Active);
    assert_eq!(
        run.steps[WAREHOUSE].phase,
        StepPhase::Done,
        "a finished phase stays finished"
    );
    assert_eq!(
        run.caption(),
        "run #1 · step 3 of 3 · consume the code graph"
    );
    assert_eq!(
        run.generation, 1,
        "and it is still the SAME run — no phantom restart"
    );

    // …and the next refine tick still finishes it. Without this the test passes for an
    // `observe` that ignores every poll after the first.
    let done = progress_fixture(&ws, N, N, 0, 0, known(1_284), known(1));
    observe(&ws, &done, "poll").expect("in order");
    assert!(
        board().get(&job_id(&ws)).unwrap().is_complete(),
        "the run can still finish"
    );
}

/// **`observe` hands the fault back rather than swallowing it.**
///
/// The board goes red on its own — that is fix-by-construction and it is tested in
/// step-board — but the sentence naming the two racing steps has to reach the pane, or
/// nobody learns WHICH producers collided. The signature is the contract.
#[test]
fn observe_returns_the_fault_rather_than_swallowing_it() {
    let ws = clean("ws-observe");
    let cloning = progress_fixture(&ws, N, 12, 3, 0, Counter::Pending, Counter::Pending);

    // The return type is a Result the caller must look at — asserted so a refactor to
    // `-> ()` fails a test rather than a review.
    let out: Result<(), OrderFault> = observe(&ws, &cloning, "Re-sync clicked");
    assert!(out.is_ok(), "an orderly first poll: {out:?}");

    // A second OWNER announces the build over the running clone — the real shape, since
    // `observe` claims its own predecessor and so cannot fault against itself.
    let err = board()
        .enter(&job_id(&ws), WAREHOUSE)
        .expect_err("the board refuses a build announced over a running clone");
    assert!(err.message().contains("OUT OF ORDER"), "{}", err.message());

    // The bar is already red. `observe`'s next poll must not paper over it by quietly
    // re-entering a step: a run that faulted stays faulted until it restarts.
    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(run.steps[WAREHOUSE].phase, StepPhase::Failed);
    assert!(run.caption().contains("FAILED"), "{}", run.caption());
}

// ── 4. FAILURE, RESTART, IDLE ─────────────────────────────────────────────────

/// **A member that did not clone fails the CLONE step and names it.** The chain must not
/// walk past a phase that failed.
#[test]
fn a_failed_member_reds_the_clone_step_and_names_the_member() {
    let ws = clean("ws-failed");
    let p = progress_fixture(&ws, N, 16, 0, 3, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("in order");

    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(run.steps[CLONE].phase, StepPhase::Failed);
    assert_eq!(
        run.steps[WAREHOUSE].phase,
        StepPhase::Pending,
        "the chain did not walk past it"
    );
    assert!(!run.is_complete());

    let err = run.error.clone().unwrap_or_default();
    assert!(
        err.contains("3 of 19"),
        "the ratio, not a bare count: {err}"
    );
    assert!(
        err.contains("znippy"),
        "the first failing member, by name: {err}"
    );
    assert!(
        err.contains("Connection timed out"),
        "the server's own error chain: {err}"
    );
    assert!(run.caption().contains("FAILED"), "{}", run.caption());
}

/// **A populate that starts again reads as a new run, not as lost ground.**
#[test]
fn a_restart_bumps_the_run_number_and_clears_the_old_count() {
    let ws = clean("ws-restart");
    let done = progress_fixture(&ws, N, N, 0, 0, known(1_284), known(1));
    observe(&ws, &done, "Re-sync clicked").expect("in order");
    assert_eq!(board().get(&job_id(&ws)).unwrap().generation, 1);

    // A day later, new commits: members start fetching again.
    let again = progress_fixture(&ws, N, 2, 5, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &again, "nornir's poll loop found new commits").expect("in order");

    let run = board().get(&job_id(&ws)).unwrap();
    assert_eq!(
        run.generation, 2,
        "'step 1 of 3' after '3 of 3' must read as run #2"
    );
    assert_eq!(
        run.steps[CLONE].count,
        Some((2, N as u64)),
        "measuring the NEW run"
    );
    assert!(
        run.reason_line().contains("new commits"),
        "{}",
        run.reason_line()
    );
    assert_eq!(
        run.steps[CODE_GRAPH].phase,
        StepPhase::Pending,
        "the old completion is gone"
    );
}

/// **An idle warehouse gets no run at all** — a bar drawn for work that is not happening
/// is the thing this module removes.
#[test]
fn an_idle_warehouse_starts_no_run() {
    let ws = clean("quiet");
    let idle = progress_fixture(&ws, 0, 0, 0, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &idle, "poll").expect("no run, no fault");
    assert!(board().get(&job_id(&ws)).is_none(), "no members ⇒ no bar");
}

/// **Repeated identical polls do not churn the board.** `observe` runs every 1.5 s; if it
/// rewrote the run each time, `updated_unix` would be a clock rather than a transition
/// and the bar would flicker.
#[test]
fn an_unchanged_poll_writes_nothing() {
    let ws = clean("ws-steady");
    let p = progress_fixture(&ws, N, N, 0, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("in order");
    let first = board().get(&job_id(&ws)).unwrap();

    observe(&ws, &p, "poll").expect("in order");
    observe(&ws, &p, "poll").expect("in order");
    let after = board().get(&job_id(&ws)).unwrap();

    assert_eq!(after.generation, first.generation, "no phantom restart");
    assert_eq!(
        after.steps, first.steps,
        "no phase churn on an unchanged poll"
    );
}

/// The Jobs-pane row carries the producer's own sentences, verbatim.
#[test]
fn the_job_row_carries_the_producers_sentences() {
    let ws = clean("ws-jobrow");
    let p = progress_fixture(&ws, N, 12, 3, 0, Counter::Pending, Counter::Pending);
    observe(&ws, &p, "Re-sync clicked").expect("in order");

    let run = board().get(&job_id(&ws)).unwrap();
    let rows = populate_steps::job_entries();
    let row = rows
        .iter()
        .find(|r| r.id == run.id)
        .expect("one row per populate");
    let meter = row.steps_meter().expect("the row carries a step meter");

    assert_eq!(
        meter.caption_text(),
        run.caption(),
        "drawn sentence == producer's sentence"
    );
    assert_eq!(
        meter.steps[CLONE].count,
        Some((12, N as u64)),
        "the count survives the trip"
    );
    assert_eq!(row.kind, "populate");
}