ashlar_debug/
transport.rs1use std::time::Duration;
9
10use ashlar_control::{Client, ControlError};
11use serde_json::Value;
12
13use crate::{Ctx, Env, Exit, Fail, Io};
14
15pub(crate) const MAX_PAUSE_MS: u64 = 600_000;
17
18pub trait Transport {
20 fn call(&mut self, method: &str, params: Value) -> Result<Value, ControlError>;
22
23 fn pause(&mut self, ms: u64) {
25 std::thread::sleep(Duration::from_millis(ms.min(MAX_PAUSE_MS)));
26 }
27
28 fn set_timeout(&mut self, _timeout: Duration) {}
30
31 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#[derive(Debug, Clone, PartialEq)]
53pub struct Report {
54 pub passed: usize,
56 pub steps: Vec<Value>,
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub struct ScriptError {
63 pub error: String,
65 pub detail: Option<Value>,
67 pub usage: bool,
69}
70
71impl ScriptError {
72 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
86pub 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 #[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}