1use crate::address::{InterfaceKind, ResourceAddress};
2use crate::error::{Error, Result};
3use crate::identity::Idn;
4use crate::kind::InstrumentKind;
5use crate::transport::{Transport, TransportIdentity};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(tag = "op", rename_all = "snake_case")]
14pub enum ScriptStep {
15 Write { data: String },
16 Read { data: String },
17 Clear,
18}
19
20#[derive(Debug, Clone)]
22pub struct MockTransport {
23 script: Vec<ScriptStep>,
24 steps: Arc<Mutex<Vec<ScriptStep>>>,
25 step_index: Arc<Mutex<usize>>,
26 identity: TransportIdentity,
27 fail_writes_remaining: Arc<Mutex<u32>>,
28}
29
30impl MockTransport {
31 pub fn from_script(steps: Vec<ScriptStep>) -> Self {
32 Self {
33 script: steps.clone(),
34 steps: Arc::new(Mutex::new(steps)),
35 step_index: Arc::new(Mutex::new(0)),
36 identity: TransportIdentity::default(),
37 fail_writes_remaining: Arc::new(Mutex::new(0)),
38 }
39 }
40
41 pub fn reopen(&self) -> Self {
43 let mut t = Self::from_script(self.script.clone());
44 t.identity = self.identity.clone();
45 *t.fail_writes_remaining.lock().unwrap() = *self.fail_writes_remaining.lock().unwrap();
46 t
47 }
48
49 pub fn script(&self) -> &[ScriptStep] {
50 &self.script
51 }
52
53 pub fn with_identity(mut self, identity: TransportIdentity) -> Self {
54 self.identity = identity;
55 self
56 }
57
58 pub fn fail_writes(self, count: u32) -> Self {
60 *self.fail_writes_remaining.lock().unwrap() = count;
61 self
62 }
63
64 fn next_step(&self) -> Result<ScriptStep> {
65 let mut idx = self.step_index.lock().unwrap();
66 let steps = self.steps.lock().unwrap();
67 if *idx >= steps.len() {
68 return Err(Error::MockExhausted);
69 }
70 let step = steps[*idx].clone();
71 *idx += 1;
72 Ok(step)
73 }
74}
75
76impl Transport for MockTransport {
77 fn write(&mut self, data: &[u8]) -> Result<()> {
78 let mut fails = self.fail_writes_remaining.lock().unwrap();
79 if *fails > 0 {
80 *fails -= 1;
81 return Err(Error::Timeout);
82 }
83 drop(fails);
84
85 match self.next_step()? {
86 ScriptStep::Write { data: expected } => {
87 let actual = String::from_utf8_lossy(data).to_string();
88 if normalize_cmd(&actual) != normalize_cmd(&expected) {
89 return Err(Error::MockMismatch { expected, actual });
90 }
91 Ok(())
92 }
93 other => Err(Error::MockMismatch {
94 expected: format!("write, got {other:?}"),
95 actual: String::from_utf8_lossy(data).to_string(),
96 }),
97 }
98 }
99
100 fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
101 match self.next_step()? {
102 ScriptStep::Read { data } => {
103 let bytes = data.as_bytes();
104 let n = bytes.len().min(buf.len());
105 buf[..n].copy_from_slice(&bytes[..n]);
106 Ok(n)
107 }
108 other => Err(Error::MockMismatch {
109 expected: "read".into(),
110 actual: format!("{other:?}"),
111 }),
112 }
113 }
114
115 fn clear(&mut self) -> Result<()> {
116 match self.next_step()? {
117 ScriptStep::Clear => Ok(()),
118 other => Err(Error::MockMismatch {
119 expected: "clear".into(),
120 actual: format!("{other:?}"),
121 }),
122 }
123 }
124
125 fn set_read_timeout(&mut self, _timeout: Duration) -> Result<()> {
126 Ok(())
127 }
128
129 fn identity(&self) -> TransportIdentity {
130 self.identity.clone()
131 }
132}
133
134#[cfg(feature = "async")]
135impl crate::async_transport::AsyncTransport for MockTransport {
136 fn write<'a>(
137 &'a mut self,
138 data: &'a [u8],
139 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
140 Box::pin(async move { Transport::write(self, data) })
141 }
142
143 fn read<'a>(
144 &'a mut self,
145 buf: &'a mut [u8],
146 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<usize>> + Send + 'a>> {
147 Box::pin(async move { Transport::read(self, buf) })
148 }
149
150 fn clear<'a>(
151 &'a mut self,
152 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
153 Box::pin(async move { Transport::clear(self) })
154 }
155
156 fn set_read_timeout<'a>(
157 &'a mut self,
158 timeout: Duration,
159 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
160 Box::pin(async move { Transport::set_read_timeout(self, timeout) })
161 }
162
163 fn identity(&self) -> TransportIdentity {
164 Transport::identity(self)
165 }
166}
167
168fn normalize_cmd(s: &str) -> String {
169 s.trim().to_uppercase()
170}
171
172#[derive(Debug, Default)]
174pub struct ScriptedFixture {
175 idn: Idn,
176 kinds: Vec<InstrumentKind>,
177 query_handlers: HashMap<String, String>,
178 write_handlers: Vec<String>,
179}
180
181impl ScriptedFixture {
182 pub fn builder() -> ScriptedFixtureBuilder {
183 ScriptedFixtureBuilder::default()
184 }
185
186 pub fn into_transport(self) -> MockTransport {
187 let mut steps = Vec::new();
188
189 for cmd in &self.write_handlers {
190 let data = if cmd.ends_with('\n') {
191 cmd.clone()
192 } else {
193 format!("{cmd}\n")
194 };
195 steps.push(ScriptStep::Write { data });
196 }
197
198 for (query, response) in &self.query_handlers {
199 steps.push(ScriptStep::Write {
200 data: format!("{query}\n"),
201 });
202 let resp = if response.ends_with('\n') {
203 response.clone()
204 } else {
205 format!("{response}\n")
206 };
207 steps.push(ScriptStep::Read { data: resp });
208 }
209
210 let identity = TransportIdentity {
211 manufacturer: Some(self.idn.manufacturer.clone()),
212 model: Some(self.idn.model.clone()),
213 serial: Some(self.idn.serial.clone()),
214 interface: InterfaceKind::Unknown,
215 manf_id: None,
216 model_code: None,
217 };
218
219 MockTransport::from_script(steps).with_identity(identity)
220 }
221
222 pub fn kinds(&self) -> &[InstrumentKind] {
223 &self.kinds
224 }
225
226 pub fn idn(&self) -> &Idn {
227 &self.idn
228 }
229}
230
231#[derive(Debug, Default)]
232pub struct ScriptedFixtureBuilder {
233 idn: Idn,
234 kinds: Vec<InstrumentKind>,
235 query_handlers: HashMap<String, String>,
236 write_handlers: Vec<String>,
237}
238
239impl ScriptedFixtureBuilder {
240 pub fn idn(mut self, manufacturer: &str, model: &str, serial: &str, firmware: &str) -> Self {
241 self.idn = Idn {
242 manufacturer: manufacturer.into(),
243 model: model.into(),
244 serial: serial.into(),
245 firmware: firmware.into(),
246 };
247 self
248 }
249
250 pub fn kinds(mut self, kinds: impl IntoIterator<Item = InstrumentKind>) -> Self {
251 self.kinds = kinds.into_iter().collect();
252 self
253 }
254
255 pub fn on_query(mut self, query: &str, response: &str) -> Self {
256 self.query_handlers
257 .insert(query.to_string(), response.to_string());
258 self
259 }
260
261 pub fn on_write(mut self, command: &str) -> Self {
263 self.write_handlers.push(command.to_string());
264 self
265 }
266
267 pub fn with_idn_probe(mut self) -> Self {
269 self.query_handlers
270 .insert("*IDN?".into(), self.idn.format_response());
271 self
272 }
273
274 pub fn build(self) -> ScriptedFixture {
275 ScriptedFixture {
276 idn: self.idn,
277 kinds: self.kinds,
278 query_handlers: self.query_handlers,
279 write_handlers: self.write_handlers,
280 }
281 }
282}
283
284pub fn mock_address(name: &str) -> Result<ResourceAddress> {
286 ResourceAddress::parse(&format!("mock://{name}"))
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct Transcript {
292 pub steps: Vec<ScriptStep>,
293}
294
295impl Transcript {
296 pub fn from_steps(steps: Vec<ScriptStep>) -> Self {
297 Self { steps }
298 }
299
300 pub fn to_json(&self) -> Result<String> {
301 serde_json::to_string_pretty(self).map_err(|e| Error::Parse(e.to_string()))
302 }
303
304 pub fn from_json(json: &str) -> Result<Self> {
305 serde_json::from_str(json).map_err(|e| Error::Parse(e.to_string()))
306 }
307}