Skip to main content

ashlar_debug/
transport.rs

1//! What a `.ui` script talks to: the TCP control port ([`Client`]) or anything else that answers
2//! control methods, such as an in-process app driver on a virtual clock.
3//!
4//! [`run_script`] runs a script against any [`Transport`]. `sleep N` and `wait N` call
5//! [`Transport::pause`], so a driver with virtual time lets that time pass instead of the wall
6//! clock, and the same script gives the same steps on every run.
7
8use std::time::Duration;
9
10use ashlar_control::{Client, ControlError};
11use serde_json::Value;
12
13use crate::{Ctx, Env, Exit, Fail, Io};
14
15/// Longest pause one `sleep`/`wait N` line asks for.
16pub(crate) const MAX_PAUSE_MS: u64 = 600_000;
17
18/// Something that answers control methods.
19pub trait Transport {
20    /// Call one method and return its result, or the app's error.
21    fn call(&mut self, method: &str, params: Value) -> Result<Value, ControlError>;
22
23    /// Let `ms` milliseconds pass (`sleep`, `wait <ms>`). The default waits on the wall clock.
24    fn pause(&mut self, ms: u64) {
25        std::thread::sleep(Duration::from_millis(ms.min(MAX_PAUSE_MS)));
26    }
27
28    /// Longest wait for one reply, where that means something (a socket).
29    fn set_timeout(&mut self, _timeout: Duration) {}
30
31    /// The TCP port, when there is one.
32    fn port(&self) -> Option<u16> {
33        None
34    }
35}
36
37impl Transport for Client {
38    fn call(&mut self, method: &str, params: Value) -> Result<Value, ControlError> {
39        Client::call(self, method, params)
40    }
41
42    fn set_timeout(&mut self, timeout: Duration) {
43        Client::set_timeout(self, timeout);
44    }
45
46    fn port(&self) -> Option<u16> {
47        Some(Client::port(self))
48    }
49}
50
51/// A script that ran to the end.
52#[derive(Debug, Clone, PartialEq)]
53pub struct Report {
54    /// Steps run (blank and comment lines do not count).
55    pub passed: usize,
56    /// One `{"line", "command", "ok", "result"}` object per step.
57    pub steps: Vec<Value>,
58}
59
60/// A script that stopped.
61#[derive(Debug, Clone, PartialEq)]
62pub struct ScriptError {
63    /// `file:line: what went wrong`.
64    pub error: String,
65    /// The failing line, the steps before it and the app's detail, when the script ran.
66    pub detail: Option<Value>,
67    /// The script could not run as written (a bad command or argument), not a failed step.
68    pub usage: bool,
69}
70
71impl ScriptError {
72    /// The exit code `ashlar-debug batch` gives for this failure.
73    pub fn exit(&self) -> Exit {
74        if self.usage { Exit::Usage } else { Exit::Failed }
75    }
76}
77
78impl std::fmt::Display for ScriptError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.write_str(&self.error)
81    }
82}
83
84impl std::error::Error for ScriptError {}
85
86/// Run a `.ui` script (the `batch` language) against `transport`. `name` labels lines in errors.
87/// Commands that manage sessions or processes (`open`, `connect`, `close`, `trace`, …) fail here:
88/// the transport is the target.
89pub fn run_script(transport: &mut dyn Transport, name: &str, text: &str) -> Result<Report, ScriptError> {
90    let env = Env::under(std::path::Path::new(""));
91    let (mut input, mut out, mut err) = (&b""[..], std::io::sink(), std::io::sink());
92    let mut io = Io { input: &mut input, out: &mut out, err: &mut err };
93    let mut ctx = Ctx::new(&env, &mut io, true);
94    ctx.inproc = Some(transport);
95    ctx.in_batch = true;
96    match ctx.run_script(name, text, false) {
97        Ok(done) => {
98            let passed = done.result.get("passed").and_then(Value::as_u64).map_or(0, |n| usize::try_from(n).unwrap_or(usize::MAX));
99            let steps = done.result.get("steps").and_then(Value::as_array).cloned().unwrap_or_default();
100            Ok(Report { passed, steps })
101        }
102        Err(Fail::Failed { error, detail, .. }) => Err(ScriptError { error, detail, usage: false }),
103        Err(Fail::Usage(error, _)) => Err(ScriptError { error, detail: None, usage: true }),
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    #![allow(clippy::unwrap_used, clippy::expect_used)]
110    use super::*;
111    use serde_json::json;
112
113    /// Answers `ui.get` with the virtual time; `pause` moves it.
114    #[derive(Default)]
115    struct Fake {
116        ms: u64,
117        calls: Vec<String>,
118    }
119
120    impl Transport for Fake {
121        fn call(&mut self, method: &str, _params: Value) -> Result<Value, ControlError> {
122            self.calls.push(method.to_string());
123            match method {
124                "ui.get" => Ok(json!({"value": self.ms.to_string()})),
125                "app.version" => Ok(json!({"name": "fake", "version": "1"})),
126                _ => Err(ControlError::Remote { error: format!("unknown method {method}"), detail: None }),
127            }
128        }
129        fn pause(&mut self, ms: u64) {
130            self.ms += ms;
131        }
132    }
133
134    #[test]
135    fn sleep_and_wait_pass_virtual_time() {
136        let mut f = Fake::default();
137        let r = run_script(&mut f, "t.ui", "# virtual\nsleep 1000\nwait 500\nassert text id=clock 1500\n").unwrap();
138        assert_eq!(r.passed, 3);
139        assert_eq!(r.steps[2]["result"]["got"], "1500");
140        assert_eq!(f.ms, 1500);
141    }
142
143    #[test]
144    fn failures_name_the_line_and_session_verbs_are_refused() {
145        let mut f = Fake::default();
146        let e = run_script(&mut f, "t.ui", "sleep 10\nassert text id=clock 0\n").unwrap_err();
147        assert!(e.error.starts_with("t.ui:2: expected text of id=clock"), "{e}");
148        assert_eq!(e.exit(), Exit::Failed);
149        assert_eq!(e.detail.as_ref().unwrap()["passed"], 1);
150        f.calls.clear();
151        for verb in ["open app", "connect 1", "close", "trace start", "sessions"] {
152            let e = run_script(&mut f, "t.ui", verb).unwrap_err();
153            assert!(e.error.contains("not available in an in-process script"), "{verb}: {e}");
154        }
155        assert!(f.calls.is_empty(), "{:?}", f.calls);
156    }
157}