kenwood_thd75/transport/
mock.rs1use std::collections::VecDeque;
4use std::path::Path;
5
6use crate::error::TransportError;
7
8use super::Transport;
9
10#[derive(Debug)]
12pub struct MockTransport {
13 exchanges: VecDeque<(Vec<u8>, Vec<u8>)>,
14 pending_response: Option<Vec<u8>>,
15 accept_any_write: bool,
16}
17
18impl MockTransport {
19 #[must_use]
21 pub const fn new() -> Self {
22 Self {
23 exchanges: VecDeque::new(),
24 pending_response: None,
25 accept_any_write: false,
26 }
27 }
28
29 pub fn expect(&mut self, command: &[u8], response: &[u8]) {
34 self.exchanges
35 .push_back((command.to_vec(), response.to_vec()));
36 }
37
38 pub fn from_fixture(path: &Path) -> Result<Self, std::io::Error> {
47 let content = std::fs::read_to_string(path)?;
48 let mut mock = Self::new();
49 let mut current_command: Option<Vec<u8>> = None;
50
51 for line in content.lines() {
52 if let Some(cmd) = line.strip_prefix("> ") {
53 let bytes = cmd.replace("\\r", "\r").into_bytes();
54 current_command = Some(bytes);
55 } else if let Some(resp) = line.strip_prefix("< ") {
56 let bytes = resp.replace("\\r", "\r").into_bytes();
57 if let Some(cmd) = current_command.take() {
58 mock.exchanges.push_back((cmd, bytes));
59 }
60 }
61 }
62
63 Ok(mock)
64 }
65
66 pub fn queue_read(&mut self, data: &[u8]) {
72 self.pending_response = Some(data.to_vec());
73 }
74
75 pub const fn expect_any_write(&mut self) {
80 self.accept_any_write = true;
81 }
82
83 pub fn assert_complete(&self) {
89 assert!(
90 self.exchanges.is_empty(),
91 "MockTransport has {} unconsumed exchange(s)",
92 self.exchanges.len()
93 );
94 }
95}
96
97impl Default for MockTransport {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl Transport for MockTransport {
104 async fn write(&mut self, data: &[u8]) -> Result<(), TransportError> {
105 tracing::debug!(bytes = data.len(), "mock: write");
106
107 if self.accept_any_write && self.exchanges.is_empty() {
108 tracing::debug!("mock: accepting any write (no response queued)");
109 return Ok(());
110 }
111
112 let (expected_cmd, response) = self.exchanges.pop_front().ok_or_else(|| {
113 TransportError::Write(std::io::Error::new(
114 std::io::ErrorKind::InvalidData,
115 format!(
116 "no more expected exchanges, but got write: {:?}",
117 String::from_utf8_lossy(data)
118 ),
119 ))
120 })?;
121
122 if data != expected_cmd {
123 return Err(TransportError::Write(std::io::Error::new(
124 std::io::ErrorKind::InvalidData,
125 format!(
126 "expected command {:?}, got {:?}",
127 String::from_utf8_lossy(&expected_cmd),
128 String::from_utf8_lossy(data)
129 ),
130 )));
131 }
132
133 tracing::debug!(bytes = response.len(), "mock: read response queued");
134 self.pending_response = Some(response);
135 Ok(())
136 }
137
138 async fn read(&mut self, buf: &mut [u8]) -> Result<usize, TransportError> {
139 let response = self.pending_response.take().ok_or_else(|| {
140 TransportError::Read(std::io::Error::new(
141 std::io::ErrorKind::WouldBlock,
142 "no pending response — call write() first",
143 ))
144 })?;
145
146 let len = response.len().min(buf.len());
147 buf[..len].copy_from_slice(&response[..len]);
148 tracing::debug!(bytes = len, "mock: read");
149 Ok(len)
150 }
151
152 async fn close(&mut self) -> Result<(), TransportError> {
153 tracing::debug!("mock: closing transport");
154 self.exchanges.clear();
155 self.pending_response = None;
156 Ok(())
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[tokio::test]
165 async fn basic_exchange() {
166 let mut mock = MockTransport::new();
167 mock.expect(b"ID\r", b"ID TH-D75\r");
168 mock.write(b"ID\r").await.unwrap();
169 let mut buf = [0u8; 64];
170 let n = mock.read(&mut buf).await.unwrap();
171 assert_eq!(&buf[..n], b"ID TH-D75\r");
172 mock.assert_complete();
173 }
174
175 #[tokio::test]
176 async fn unexpected_command() {
177 let mut mock = MockTransport::new();
178 mock.expect(b"ID\r", b"ID TH-D75\r");
179 let result = mock.write(b"FV\r").await;
180 assert!(result.is_err());
181 }
182
183 #[tokio::test]
184 async fn multiple_exchanges() {
185 let mut mock = MockTransport::new();
186 mock.expect(b"ID\r", b"ID TH-D75\r");
187 mock.expect(b"FV\r", b"FV 1.03.000\r");
188
189 mock.write(b"ID\r").await.unwrap();
190 let mut buf = [0u8; 64];
191 let n = mock.read(&mut buf).await.unwrap();
192 assert_eq!(&buf[..n], b"ID TH-D75\r");
193
194 mock.write(b"FV\r").await.unwrap();
195 let n = mock.read(&mut buf).await.unwrap();
196 assert_eq!(&buf[..n], b"FV 1.03.000\r");
197
198 mock.assert_complete();
199 }
200
201 #[tokio::test]
202 async fn from_fixture_file() {
203 let mut mock =
204 MockTransport::from_fixture(Path::new("tests/fixtures/identify.txt")).unwrap();
205
206 mock.write(b"ID\r").await.unwrap();
207 let mut buf = [0u8; 64];
208 let n = mock.read(&mut buf).await.unwrap();
209 assert_eq!(&buf[..n], b"ID TH-D75\r");
210
211 mock.write(b"FV\r").await.unwrap();
212 let n = mock.read(&mut buf).await.unwrap();
213 assert_eq!(&buf[..n], b"FV 1.03.000\r");
214
215 mock.assert_complete();
216 }
217
218 #[tokio::test]
219 async fn read_without_write_errors() {
220 let mut mock = MockTransport::new();
221 let mut buf = [0u8; 64];
222 let result = mock.read(&mut buf).await;
223 assert!(result.is_err());
224 }
225
226 #[tokio::test]
227 async fn write_with_no_exchanges_errors() {
228 let mut mock = MockTransport::new();
229 let result = mock.write(b"ID\r").await;
230 assert!(result.is_err());
231 }
232
233 #[tokio::test]
234 async fn default_creates_empty() {
235 let mock = MockTransport::default();
236 mock.assert_complete();
237 }
238
239 #[tokio::test]
240 async fn close_clears_state() {
241 let mut mock = MockTransport::new();
242 mock.expect(b"ID\r", b"ID TH-D75\r");
243 mock.close().await.unwrap();
244 mock.assert_complete();
245 }
246}