Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-fleet/src/bin/fleet_mock_server.rs

//! **`fleet_mock_server`** — a TRIVIAL mounted app for proving [`LocalProcessSpinner`].
//!
//! This is the lightweight stand-in the spinner test launches instead of the five
//! real apps (spawning all of them in a unit test is too heavy / flaky — the
//! `run_local_fleet` example is the real-apps entry). It mounts a
//! [`CmdJsonServer`](nornir_robotui::cmd_json::CmdJsonServer) over a hand-rolled
//! [`CmdHost`] (a bare click-counter — NO egui / kittest, so it builds with the lean
//! `wire` feature and no dev-deps) on a Unix socket, mirroring dwarves-gui's
//! `--robot-remote <socket>` mount so the spinner drives it exactly like a real app.
//!
//! ## CLI (mirrors the real apps)
//! * `fleet_mock_server --robot-remote <socket>` — bind `<socket>`, serve drivers
//!   sequentially until each hangs up (EOF). Own blocking thread, NO `thread::spawn`.
//! * `fleet_mock_server scenarios` — emit a tiny per-tab scenario library as the
//!   SAME `serde_json` wire form the fleet loads (`<member>.scenarios.json`).
//!
//! ROOT LAW #0: plain blocking accept→serve loop; spawns nothing.

#[cfg(unix)]
fn main() {
    use std::io::BufReader;
    use std::os::unix::net::UnixListener;

    use nornir_robotui::cmd_json::{CmdHost, CmdJsonServer, IoChannel};
    use nornir_robotui::{Action, Assert, Scenario, Step as UiStep, Target};
    use serde_json::{json, Value};

    /// A bare click-counter host — the whole app surface the mock needs: a `Bump`
    /// button that increments `/clicks`, and `state_json` a driver reads/asserts.
    /// Implements [`CmdHost`] by hand (no egui), so a driver drives it over the wire
    /// exactly like a real `RobotSession`-backed mount.
    #[derive(Default)]
    struct StubHost {
        clicks: i64,
    }

    impl StubHost {
        fn state_value(&self) -> Value {
            json!({ "clicks": self.clicks })
        }
    }

    impl CmdHost for StubHost {
        fn locate(&self, _target: &Target) -> Option<u64> {
            Some(1) // any button resolves — the mock has one control
        }
        fn act(&mut self, action: &Action) -> Result<(), String> {
            match action {
                Action::ClickButton(_) | Action::ClickLabel(_) | Action::ClickById(_) => {
                    self.clicks += 1;
                    Ok(())
                }
                _ => Ok(()),
            }
        }
        fn assert(&self, assert: &Assert) -> Result<(), String> {
            match assert {
                Assert::None | Assert::Drew => Ok(()),
                // A chained `.assert(a).assert(b)` arrives as one `All`. Evaluate
                // EVERY clause — the catch-all below would otherwise turn a
                // perfectly evaluatable chain into "mock host does not evaluate".
                Assert::All(clauses) => {
                    let n = clauses.len();
                    for (i, c) in clauses.iter().enumerate() {
                        self.assert(c)
                            .map_err(|e| format!("clause {} of {n}: {e}", i + 1))?;
                    }
                    Ok(())
                }
                Assert::State { pointer, expected } => {
                    let got = self.state_value().pointer(pointer).cloned();
                    if got.as_ref() == Some(expected) {
                        Ok(())
                    } else {
                        Err(format!("expected {expected} at {pointer}, got {got:?}"))
                    }
                }
                other => Err(format!("mock host does not evaluate {other:?}")),
            }
        }
        fn state(&self) -> Value {
            self.state_value()
        }
        fn drive_step(&mut self, step: &UiStep) -> (bool, String) {
            if let Err(e) = self.act(&step.action) {
                return (false, e);
            }
            match self.assert(&step.assert) {
                Ok(()) => (true, String::new()),
                Err(e) => (false, e),
            }
        }
    }

    /// The mock's tiny recorded scenario library (the `scenarios` emit path): one
    /// clean tab that bumps the counter to 1.
    fn mock_scenarios() -> Vec<Scenario> {
        vec![Scenario::new("ok_tab").step(
            UiStep::new("bump → clicks == 1")
                .action(Action::ClickButton("Bump".into()))
                .assert(Assert::State { pointer: "/clicks".into(), expected: json!(1) }),
        )]
    }

    /// MOUNT the server on `path` and serve drivers sequentially (one at a time)
    /// until an `accept` error. Clears a stale socket first (re-bind clean).
    fn serve(path: &std::path::Path) -> std::io::Result<()> {
        let _ = std::fs::remove_file(path);
        let listener = UnixListener::bind(path)?;
        let mut server = CmdJsonServer::new(StubHost::default());
        eprintln!("fleet_mock_server: listening on {}", path.display());
        loop {
            let (stream, _) = listener.accept()?;
            let rd = stream.try_clone()?;
            let mut chan = IoChannel::new(BufReader::new(rd), stream);
            if let Err(e) = server.serve(&mut chan) {
                eprintln!("fleet_mock_server: connection ended: {e}");
            }
        }
    }

    let mut args = std::env::args().skip(1);
    while let Some(a) = args.next() {
        match a.as_str() {
            "scenarios" => {
                let json = serde_json::to_string_pretty(&mock_scenarios()).unwrap();
                println!("{json}");
                return;
            }
            "--robot-remote" => {
                let Some(path) = args.next() else {
                    eprintln!("fleet_mock_server: --robot-remote needs a <socket> path");
                    std::process::exit(2);
                };
                if let Err(e) = serve(std::path::Path::new(&path)) {
                    eprintln!("fleet_mock_server: serve failed: {e}");
                    std::process::exit(1);
                }
                return;
            }
            other => {
                eprintln!("fleet_mock_server: unknown arg `{other}`");
                std::process::exit(2);
            }
        }
    }
    eprintln!("usage: fleet_mock_server (--robot-remote <socket> | scenarios)");
    std::process::exit(2);
}

#[cfg(not(unix))]
fn main() {
    eprintln!("fleet_mock_server: Unix-socket only");
    std::process::exit(1);
}