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