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/robot_pipeline_runs.rs

//! **ROBOT-UI: the Pipeline tab shows the selected pipeline's real runs** — read from a
//! real forge, never from this machine's disk.
//!
//! ```text
//!   RunaStepRunner + the real local backend (two runs of `ws.runa.ron`; the second's
//!   `verify` step fails and says why)  ──▶  warehouse (history, step logs, units)
//!   forge (`dwarf.v1`: History · GetRunReport · StepLog)  ──▶  🔎 Pipeline · runs
//! ```
//!
//! The pane shows the runs newest first, a run's stages and steps with status and
//! duration, a `BuildWorkspace` step's units, and — a click on the failed step — the
//! tail of its log on screen. A pipeline that never ran says so.

use std::sync::{Arc, Mutex};
use std::time::Duration;

use dwarves_gui::pipeline::PipelinePane;
use egui_kittest::kittest::Queryable;
use facett_core::Facet;
use serde_json::{json, Value};

struct Board;

impl dwarves::unit_tree::BoardSource for Board {
    fn board(&self, _: &str) -> Result<Value, String> {
        Ok(json!({
            "members": [{ "member": "skade", "phase": "landed" }],
            "units": [{ "key": "k", "package": "path+ws:skade/skade#0.7.0", "target": "lib:skade", "mode": "check", "test": false,
                        "features": [], "members": ["skade"], "shared_by": 1, "state": "done", "deps": [] }],
        }))
    }
    fn jobs(&self, _: &str) -> Vec<Value> {
        Vec::new()
    }
}

const PIPELINE: &str = r#"Pipeline(name: "ws", stages: [
    Stage(name: "build", steps: [ BuildWorkspace(workspace: "nordisk") ]),
    Stage(name: "verify", needs: ["build"], steps: [ BuildWorkspace(workspace: "other") ]),
])"#;

fn steps<S>(h: &mut egui_kittest::Harness<'_, S>) {
    for _ in 0..3 {
        h.step();
    }
}

#[test]
fn the_pipeline_tab_shows_its_runs_steps_units_and_a_failed_steps_log_from_the_forge() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path().join("repo");
    std::fs::create_dir_all(&root).unwrap();
    std::fs::write(root.join("ws.runa.ron"), PIPELINE).unwrap();
    let warehouse = tmp.path().join("warehouse");

    // `nornir workspace build <ws>`: the `other` workspace fails once `fail` exists
    let bin = tmp.path().join("bin");
    std::fs::create_dir_all(&bin).unwrap();
    let script = bin.join("nornir");
    std::fs::write(
        &script,
        format!(
            "#!/bin/sh\necho \"reading workspace $3\"\nif [ \"$3\" = other ] && [ -f {fail} ]; then\n  echo 'error[E0425]: cannot find value `x` in this scope' >&2\n  echo ' --> other/src/lib.rs:3:5' >&2\n  exit 1\nfi\necho \"built $3\"\n",
            fail = tmp.path().join("fail").display()
        ),
    )
    .unwrap();
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
    // SAFETY: this test binary holds this one test; nothing else reads PATH concurrently.
    unsafe {
        std::env::set_var(
            "PATH",
            format!(
                "{}:{}",
                bin.display(),
                std::env::var("PATH").unwrap_or_default()
            ),
        );
        std::env::remove_var("NORNIR_SERVER");
        std::env::remove_var("NORNIR_SERVER_ADDR");
    }

    let pipeline = dwarves::load_pipeline(&root.join("ws.runa.ron")).unwrap();
    let run = |id: &str| {
        let sched = dwarves::ForgeBackend::discover(root.clone()).into_scheduler();
        let runner = dwarves::RunaStepRunner::new(&sched)
            .with_unit_board(Arc::new(Board))
            .with_unit_poll(Duration::from_millis(5));
        let ctx = dwarves::RunCtx {
            project: "repo".into(),
            run_id: id.into(),
            trigger: "manual".into(),
            warehouse: Some(&warehouse),
            sink: None,
        };
        runner.run(&pipeline, &ctx).unwrap()
    };
    assert!(run("run-1").is_green());
    std::thread::sleep(Duration::from_millis(20));
    std::fs::write(tmp.path().join("fail"), "").unwrap();
    assert!(!run("run-2").is_green());

    // ── the shipped pane, given only the forge ──
    let forge = dwarves::server::spawn_dwarf(warehouse.clone());
    let mut pane = PipelinePane::at(&root).with_forge(forge.endpoint.clone(), String::new());
    Facet::update_json(&mut pane, r#"{"pipeline_source":"runs"}"#);
    Facet::update_json(&mut pane, r#"{"runs_wait":true}"#);
    let s = Facet::state_json(&pane);
    let r = &s["runs"];
    assert_eq!(r["state"], "loaded", "{s}");
    let ids: Vec<&str> = r["runs"]
        .as_array()
        .unwrap()
        .iter()
        .map(|x| x["run_id"].as_str().unwrap())
        .collect();
    assert_eq!(ids, vec!["run-2", "run-1"], "newest first");
    assert_eq!(r["run_id"], "run-2", "the newest run is shown");
    let stages: Vec<&str> = r["stages"]
        .as_array()
        .unwrap()
        .iter()
        .map(|x| x["stage"].as_str().unwrap())
        .collect();
    assert_eq!(stages, vec!["build", "verify"]);
    assert_eq!(r["stages"][0]["steps"][0]["status"], "passed");
    let failed = &r["stages"][1]["steps"][0];
    assert_eq!(
        (failed["step"].as_str(), failed["status"].as_str()),
        (Some("BuildWorkspace"), Some("failed")),
        "{r}"
    );
    assert!(
        failed["duration_ms"].as_u64().unwrap() > 0,
        "a step's duration is on the run: {failed}"
    );
    assert_eq!(
        r["units"]["steps"].as_array().map(Vec::len),
        Some(2),
        "both BuildWorkspace steps carry units: {}",
        r["units"]
    );

    // ── drawn; a click on the failed step shows its log ──
    let pane = Arc::new(Mutex::new(pane));
    let p2 = pane.clone();
    let mut h = egui_kittest::Harness::builder()
        .with_size(egui::vec2(1400.0, 1100.0))
        .build_ui(move |ui| Facet::ui(&mut *p2.lock().unwrap(), ui));
    steps(&mut h);
    h.get_by_label("row: step-1").click();
    steps(&mut h);
    Facet::update_json(&mut *pane.lock().unwrap(), r#"{"runs_wait":true}"#);
    steps(&mut h);
    let s = Facet::state_json(&*pane.lock().unwrap());
    let log = &s["runs"]["log"];
    assert_eq!(s["runs"]["step"], 1, "{}", s["runs"]);
    assert!(log["found"].as_bool().unwrap(), "{log}");
    let tail = log["tail"].to_string();
    assert!(
        tail.contains("error[E0425]") && tail.contains("other/src/lib.rs"),
        "the failed step's log: {log}"
    );
    assert!(
        h.query_all_by_label_contains("error[E0425]")
            .next()
            .is_some(),
        "the log is on screen"
    );

    // a click on the older run shows it
    h.get_by_label("row: run-1").click();
    steps(&mut h);
    Facet::update_json(&mut *pane.lock().unwrap(), r#"{"runs_wait":true}"#);
    let s = Facet::state_json(&*pane.lock().unwrap());
    assert_eq!(s["runs"]["run_id"], "run-1", "{}", s["runs"]);
    assert_eq!(s["runs"]["stages"][1]["steps"][0]["status"], "passed");
    drop(h);

    // ── a pipeline that never ran says so ──
    let never = tmp.path().join("never");
    std::fs::create_dir_all(&never).unwrap();
    std::fs::write(never.join("never.runa.ron"), r#"Pipeline(name: "never", stages: [ Stage(name: "b", steps: [ BuildWorkspace(workspace: "nordisk") ]) ])"#).unwrap();
    let mut empty = PipelinePane::at(&never).with_forge(forge.endpoint.clone(), String::new());
    Facet::update_json(&mut empty, r#"{"pipeline_source":"runs"}"#);
    Facet::update_json(&mut empty, r#"{"runs_wait":true}"#);
    let e = Facet::state_json(&empty);
    assert_eq!(e["runs"]["state"], "no_runs", "{e}");
    assert!(
        e["runs"]["line"]
            .as_str()
            .unwrap()
            .contains("no runs yet: ▶ Run starts one"),
        "{e}"
    );
}