dwarves-gui/tests/robot_run_units.rs
//! **ROBOT-UI: a run's `BuildWorkspace` step shows its units — and drills into them.**
//!
//! ```text
//! RunaStepRunner (a BuildWorkspace step; the board read while it runs)
//! └▶ warehouse (the run's history + its units)
//! forge (`dwarf.v1`, over that warehouse) ──▶ the ⚒ Forge tab
//! ```
//!
//! The tab is given only the forge. For the shown run it draws the step's units —
//! compiled / fresh / failed / skipped, a row per repo (ok / partial / failed) — on the
//! shared table; a click on a repo drills into its failed units, a click on a failed unit
//! shows its error.
use std::sync::{Arc, Mutex};
use std::time::Duration;
use egui_kittest::kittest::Queryable;
use facett_core::Panel;
use serde_json::{json, Value};
struct Board(Mutex<Vec<Value>>);
impl dwarves::unit_tree::BoardSource for Board {
fn board(&self, _: &str) -> Result<Value, String> {
let mut q = self.0.lock().unwrap();
Ok(if q.len() > 1 {
q.remove(0)
} else {
q[0].clone()
})
}
fn jobs(&self, _: &str) -> Vec<Value> {
vec![json!({ "kind": "workspace_populate", "status": "failed",
"detail_json": json!({ "members": [{ "member": "knut", "error": "error[E0425]: cannot find value `x` in this scope\n --> knut/src/lib.rs:3:5" }] }).to_string() })]
}
}
fn at(s: [&str; 4], phases: [&str; 2]) -> Value {
let u = |key: &str, pkg: &str, target: &str, members: &[&str], state: &str| {
json!({ "key": key, "package": pkg, "target": target, "mode": "check", "test": false, "features": [],
"members": members, "shared_by": members.len(), "state": state, "deps": [] })
};
json!({
"members": [{ "member": "knut", "phase": phases[0] }, { "member": "skade", "phase": phases[1] }],
"units": [
u("serde", "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", "lib:serde", &["knut", "skade"], s[0]),
u("skade", "path+ws:skade/skade#0.7.0", "lib:skade", &["knut", "skade"], s[1]),
u("knut-lib", "path+ws:knut#knut@0.3.0", "lib:knut", &["knut"], s[2]),
u("knut-bin", "path+ws:knut#knut@0.3.0", "bin:knut", &["knut"], s[3]),
],
})
}
fn steps<S>(h: &mut egui_kittest::Harness<'_, S>) {
for _ in 0..3 {
h.step();
}
}
#[test]
fn the_forge_tab_shows_a_build_workspace_steps_units_and_drills_into_the_failed_ones() {
// ── a run with a BuildWorkspace step, recorded the way the runner records it ──
let tmp = tempfile::tempdir().unwrap();
let warehouse = tmp.path().join("warehouse");
let ron = tmp.path().join("ws.runa.ron");
std::fs::write(&ron, r#"Pipeline(name: "ws", stages: [ Stage(name: "build", steps: [ BuildWorkspace(workspace: "nordisk") ]) ])"#).unwrap();
let pipeline = dwarves::load_pipeline(&ron).unwrap();
let board = Arc::new(Board(Mutex::new(vec![
at(
["done", "compiling", "waiting", "queued"],
["compiling", "compiling"],
),
at(["done", "done", "failed", "waiting"], ["failed", "landed"]),
])));
let sched = dwarves::pipeline_wizard::fake_scheduler();
let runner = dwarves::RunaStepRunner::new(&sched)
.with_unit_board(board)
.with_unit_poll(Duration::from_millis(1));
let ctx = dwarves::RunCtx {
project: "p".into(),
run_id: "run-ws-1".into(),
trigger: "manual".into(),
warehouse: Some(&warehouse),
sink: None,
};
runner.run(&pipeline, &ctx).unwrap();
// ── the forge over that history, and the shipped Forge pane given only the forge ──
let forge = dwarves::server::spawn_dwarf(warehouse.clone());
let pane = Arc::new(Mutex::new(dwarves_embed::panel_with(
forge.endpoint.clone(),
String::new(),
None,
)));
let state = || pane.lock().unwrap().state_json();
let s = state();
assert_eq!(s["status"], "loaded", "{s}");
let u = &s["units"]["steps"][0];
assert_eq!(u["workspace"], "nordisk", "{s}");
assert_eq!(
u["counts"],
json!({ "compiled": 1, "fresh": 1, "failed": 1, "skipped": 1 })
);
assert_eq!(
u["line"],
"units 1 compiled · 1 fresh · 1 failed · 1 skipped"
);
let results: Vec<(String, String)> = u["repos"]
.as_array()
.unwrap()
.iter()
.map(|r| {
(
r["repo"].as_str().unwrap().into(),
r["result"].as_str().unwrap().into(),
)
})
.collect();
assert_eq!(
results,
vec![
("knut".into(), "partial".into()),
("skade".into(), "ok".into())
]
);
assert_eq!(u["drawn"], json!({ "repo_rows": 2, "failed_rows": 1 }));
// ── drawn, and driven by clicks: the repo, then its failed unit ──
let p2 = pane.clone();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(1400.0, 1000.0))
.build_ui(move |ui| p2.lock().unwrap().ui(ui));
steps(&mut h);
h.get_by_label("row: skade").click();
steps(&mut h);
let s = state();
assert_eq!(
s["units"]["repo"], "skade",
"a click on a repo drills into it: {}",
s["units"]
);
assert_eq!(
s["units"]["steps"][0]["failed_shown"],
json!([]),
"skade failed nothing"
);
h.get_by_label("row: knut").click();
steps(&mut h);
let s = state();
assert_eq!(s["units"]["repo"], "knut");
assert_eq!(
s["units"]["steps"][0]["failed_shown"][0]["crate"],
"knut@0.3.0"
);
h.get_by_label("row: 0").click();
steps(&mut h);
let s = state();
let err = s["units"]["error"].as_str().unwrap_or_default();
assert!(
err.contains("E0425") && err.contains("knut/src/lib.rs"),
"a click on a failed unit shows its error: {}",
s["units"]
);
assert!(
h.query_all_by_label_contains("E0425").next().is_some(),
"the error is on screen"
);
}
/// The live look: a warehouse a real `dwarves run` wrote (a BuildWorkspace step that
/// followed a live forge's board) served by a local forge, drawn by the Forge pane.
/// `DWARVES_LIVE_WAREHOUSE=… [DWARVES_UNITS_PNG=/tmp/units.png] [DWARVES_LIVE_UNIT=serde]
/// cargo test -p dwarves-gui --test robot_run_units live_run_units -- --ignored --nocapture`.
#[test]
#[ignore]
fn live_run_units() {
let Ok(wh) = std::env::var("DWARVES_LIVE_WAREHOUSE") else {
eprintln!("DWARVES_LIVE_WAREHOUSE not set — nothing to show");
return;
};
let warehouse = std::path::PathBuf::from(wh);
let forge = dwarves::server::spawn_dwarf(warehouse.clone());
let pane = Arc::new(Mutex::new(dwarves_embed::panel_with(
forge.endpoint.clone(),
String::new(),
None,
)));
let s = pane.lock().unwrap().state_json();
let units = &s["units"];
println!(
"{}",
serde_json::to_string_pretty(&json!({
"run": s["run_id"], "status": s["status"],
"steps": units["steps"].as_array().map(|a| a.iter().map(|u| json!({
"line": u["line"], "source": u["source"], "why": u["why"], "repos": u["repos"].as_array().map(Vec::len),
"not_ok": u["repos"].as_array().map(|r| r.iter().filter(|x| x["result"] != "ok").cloned().collect::<Vec<_>>()),
"failed_total": u["failed_total"], "notes": u["notes"],
})).collect::<Vec<_>>()),
}))
.unwrap()
);
let unit = std::env::var("DWARVES_LIVE_UNIT").unwrap_or_else(|_| "serde".into());
let proxy = dwarves::warehouse_proxy::spawn_proxy_over(
"http://127.0.0.1:9".into(),
String::new(),
Some(warehouse),
);
let a = nornir_warehouse_embed::room::ask(
&proxy.endpoint,
"",
"nordisk",
"Dwarves.UnitHistory",
&json!({ "unit": unit }),
);
println!(
"Dwarves.UnitHistory({unit}): state {} · compiled in {} run(s) · last failed {} · runs kept {}",
a.state,
a.body["compiled_in"].as_array().map_or(0, Vec::len),
a.body["last_failed"]["run_id"],
a.body["runs_kept"]
);
if let Ok(png) = std::env::var("DWARVES_UNITS_PNG") {
let p2 = pane.clone();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(1400.0, 1000.0))
.build_ui(move |ui| p2.lock().unwrap().ui(ui));
steps(&mut h);
match h.render() {
Ok(img) => {
img.save(&png).expect("write png");
println!("wrote {png}");
}
Err(e) => println!("no render: {e}"),
}
}
}