dwarves-gui/tests/robot_tune_pane.rs
//! **ROBOT-UI: the Syscall profiles pane reviews a real row and Approve runs
//! the tune approve verb.**
//!
//! The row is a copy of tunnr's committed `rusternetes-node-aya` profiles with
//! a real `kernel needs` join beside it (`crates/dwarves/tests/fixtures/tune`).
//! The runner is injected: it records what the pane asked and answers through
//! the REAL `dwarves::tune::run_request`, a poll late, so the pane is seen
//! waiting on the job before the document lands. The widgets are driven
//! through AccessKit (a click on the row, typing the note, the Approve
//! button), never through `update_json`.
//!
//! `DWARVES_TUNE_PNG=<file>` also renders the reviewed pane to a PNG.
#![cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use dwarves::tune::jobs::{JobState, TuneJob};
use dwarves::tune::{NoServedRunner, TuneArgs, TuneRequest, TuneVerb};
use dwarves_gui::tune_pane::{TunePane, TuneRunner};
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use serde_json::Value;
use egui_kittest::kittest::Queryable;
/// The text is drawn (a label, not only a control): somewhere in the AccessKit tree.
fn on_screen(robot: &mut RobotSession<'static, App>, text: &str) -> bool {
robot.harness_mut().query_all_by_label(text).next().is_some()
}
const ROW: &str = "tunnr:rusternetes-node-aya";
fn copy_dir(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for e in std::fs::read_dir(from).unwrap().flatten() {
let t = to.join(e.file_name());
if e.path().is_dir() {
copy_dir(&e.path(), &t);
} else {
std::fs::copy(e.path(), t).unwrap();
}
}
}
/// What the fake was asked, and the answers it holds back one poll.
#[derive(Default)]
struct Book {
asked: Vec<(TuneVerb, String, String)>,
answers: Vec<(String, JobState, bool)>,
/// While set, every job answers `Queued` (the test looks at the wait).
hold: bool,
}
struct FakeRunner {
checkouts: PathBuf,
book: Arc<Mutex<Book>>,
}
impl TuneRunner for FakeRunner {
fn artifacts(&self) -> Vec<String> {
dwarves::tune::artifacts_under(&self.checkouts)
}
fn submit(&self, verb: TuneVerb, artifact: &str, note: &str) -> Result<TuneJob, String> {
let mut b = self.book.lock().unwrap();
b.asked.push((verb, artifact.to_string(), note.to_string()));
let req = TuneRequest {
verb: Some(verb),
args: TuneArgs {
artifact: artifact.into(),
checkouts: Some(self.checkouts.clone()),
note: note.into(),
// The live runner leaves this to `git config user.name`.
reviewer: Some("Tester".into()),
..Default::default()
},
tool: None,
cwd: self.checkouts.clone(),
};
let mut req = req;
if verb == TuneVerb::Check {
req.verb = Some(TuneVerb::Show);
}
let state = match dwarves::tune::run_request(&req, &NoServedRunner) {
Ok(d) if verb == TuneVerb::Check => {
let mut v = serde_json::to_value(d).unwrap();
v["verb"] = "check".into();
v["ok"] = false.into();
v["drift"] = serde_json::json!([{ "program": "tunnr-cni", "libc": "musl", "call": "sysinfo", "action": "kill_process", "tests": ["check_succeeds"] }]);
v["problems"] = serde_json::json!(["drift: tunnr-cni (musl): new call sysinfo (kill_process), made by test check_succeeds"]);
JobState::Done(v)
}
Ok(d) => JobState::Done(serde_json::to_value(d).unwrap()),
Err(e) => JobState::Failed(serde_json::to_value(dwarves::tune::error_doc(verb, artifact, &e)).unwrap()),
};
let key = format!("k{}", b.asked.len());
b.answers.push((key.clone(), state, false));
Ok(TuneJob { job_id: format!("j-{}", b.asked.len()), key })
}
fn state(&self, job: &TuneJob) -> Option<JobState> {
let mut b = self.book.lock().unwrap();
let hold = b.hold;
let a = b.answers.iter_mut().find(|a| a.0 == job.key)?;
if hold {
return Some(JobState::Queued);
}
if !a.2 {
a.2 = true;
return Some(JobState::Queued);
}
Some(a.1.clone())
}
}
struct App(TunePane);
impl Observable for App {
fn state_json(&self) -> Value {
Facet::state_json(&self.0)
}
}
fn session(checkouts: PathBuf, book: Arc<Mutex<Book>>) -> RobotSession<'static, App> {
let runner = Arc::new(FakeRunner { checkouts, book });
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1600.0, 3600.0))
.build_ui_state(|ui, app: &mut App| Facet::ui(&mut app.0, ui), App(TunePane::new(runner)));
let mut robot = RobotSession::new(harness, "dwarves-gui");
robot.run(2);
robot
}
fn call<'a>(review: &'a Value, program: &str, syscall: &str) -> &'a Value {
review["programs"]
.as_array()
.unwrap()
.iter()
.find(|p| p["program"] == program)
.unwrap_or_else(|| panic!("no program {program}"))["calls"]
.as_array()
.unwrap()
.iter()
.find(|c| c["syscall"] == syscall)
.unwrap_or_else(|| panic!("no call {syscall} in {program}"))
}
#[test]
fn the_pane_reviews_a_row_and_approve_runs_the_approve_verb() {
let d = tempfile::tempdir().unwrap();
let checkouts = d.path().join("checkouts");
let repo = checkouts.join("tunnr");
std::fs::create_dir_all(repo.join(".git")).unwrap();
copy_dir(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../crates/dwarves/tests/fixtures/tune/tunnr"), &repo);
let book: Arc<Mutex<Book>> = Arc::default();
let mut robot = session(checkouts, book.clone());
// The rows are offered; nothing is asked before one is picked.
assert_eq!(robot.state_at("/artifacts"), Some(serde_json::json!([ROW])));
assert!(book.lock().unwrap().asked.is_empty());
// Pick the row: `show` is queued, the pane says so, then draws the document.
book.lock().unwrap().hold = true;
robot.click_label(ROW).expect("the row is a clickable widget");
robot.run(2);
assert_eq!(robot.state_at("/pending/verb"), Some(serde_json::json!("show")));
assert!(on_screen(&mut robot, "dwarves tune show — queued in jera (job j-1)"), "the wait is on screen");
assert!(!on_screen(&mut robot, "Approve") || robot.state_at("/review") == Some(Value::Null), "no document yet");
book.lock().unwrap().hold = false;
robot.run(3);
assert_eq!(robot.state_at("/pending"), Some(Value::Null));
assert_eq!(robot.state_at("/verb"), Some(serde_json::json!("show")));
let review = robot.state_at("/review").expect("a review");
assert_eq!(call(&review, "node-pid1", "getpid")["near_miss"], serde_json::json!(["by not_pid_1_refuses_to_start"]));
assert_eq!(call(&review, "tunnr-cni", "bpf")["options"], serde_json::json!(["CONFIG_BPF", "CONFIG_BPF_SYSCALL"]));
// What a reviewer reads is on screen, not only in the state.
for l in [
"near-miss by not_pid_1_refuses_to_start",
"never approved",
"Not covered off a VM",
"Kernel tunnr-kernel-k8s 6.12.104 x86_64",
] {
assert!(on_screen(&mut robot, l), "{l:?} is not on screen");
}
assert!(
on_screen(&mut robot,
"veto: CONFIG_FILE_LOCKING: tunnr-cni calls flock (test add_routes_a_pod_up_to_the_bpf_attach, add_without_a_subnet_is_refused, del_frees_the_pod, del_of_an_unknown_pod_succeeds)"
),
"the measured caller's veto is named"
);
let files = review["files"].as_array().unwrap();
assert_eq!(files.len(), 4);
assert!(files.iter().all(|f| f["line"] == "never approved"));
// Type a note, press Approve: the APPROVE verb is asked, with that note.
robot.type_into("Note", "the error-path calls are expected").expect("the note field");
robot.click_button("Approve").expect("the Approve button");
robot.run(3);
{
let b = book.lock().unwrap();
let verbs: Vec<TuneVerb> = b.asked.iter().map(|a| a.0).collect();
assert_eq!(verbs, [TuneVerb::Show, TuneVerb::Approve]);
assert_eq!(b.asked[1].1, ROW);
assert_eq!(b.asked[1].2, "the error-path calls are expected");
}
// The pane refreshed from approve's own document.
assert_eq!(robot.state_at("/verb"), Some(serde_json::json!("approve")));
let today = dwarves::tune::today();
let review = robot.state_at("/review").unwrap();
assert!(
review["files"].as_array().unwrap().iter().all(|f| f["line"] == format!("approved by Tester on {today}")),
"{review}"
);
assert!(on_screen(&mut robot, &format!("approved by Tester on {today}")));
let toml = std::fs::read_to_string(repo.join(".nornir/profiles/rusternetes-node-aya/approved.toml")).unwrap();
assert!(toml.contains("the error-path calls are expected") && toml.contains("reviewer = \"Tester\""), "{toml}");
// Check: the fake answers with a check document that found drift (the
// real verb needs heimdall); the pane draws the drift by call and test.
robot.click_button("Check").expect("the Check button");
robot.run(3);
assert_eq!(robot.state_at("/verb"), Some(serde_json::json!("check")));
assert_eq!(robot.state_at("/last_check/ok"), Some(serde_json::json!(false)));
assert!(on_screen(&mut robot, "Last check: findings"));
assert!(on_screen(&mut robot, "drift: tunnr-cni (musl) new call sysinfo (kill_process), made by test check_succeeds"));
// Probe without heimdall: refused by name, and the document stays.
robot.click_button("Probe").expect("the Probe button");
robot.run(3);
let err = robot.state_at("/error").and_then(|e| e.as_str().map(String::from)).unwrap_or_default();
assert!(!err.is_empty(), "probe without a built row must be refused");
assert_eq!(robot.state_at("/verb"), Some(serde_json::json!("check")));
assert!(on_screen(&mut robot, &format!("refused: {err}")));
if let Ok(png) = std::env::var("DWARVES_TUNE_PNG") {
match robot.harness_mut().render() {
Ok(img) => {
img.save(&png).expect("write png");
println!("wrote {png}");
}
Err(e) => println!("no render: {e}"),
}
}
}
#[test]
fn a_runner_that_refuses_is_said_by_name() {
let book: Arc<Mutex<Book>> = Arc::default();
let mut robot = {
let runner = Arc::new(dwarves_gui::tune_pane::NoRunner("the tune queue did not start: x".into()));
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(900.0, 400.0))
.build_ui_state(|ui, app: &mut App| Facet::ui(&mut app.0, ui), App(TunePane::new(runner)));
RobotSession::new(harness, "dwarves-gui")
};
robot.run(2);
assert!(on_screen(&mut robot, "no row has profiles (.nornir/profiles/<row>/measure.toml) under the checkouts"));
let _ = book;
}