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_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;
}