Skip to main content

can_motor_control/transport/
mock.rs

1//! In-process mock transport for tests.
2
3use std::collections::VecDeque;
4use std::os::fd::RawFd;
5use std::sync::{Arc, Mutex};
6
7use motor_codec::{BusCapabilities, CanFrame};
8
9use super::{CanBus, TransportError};
10
11/// Record of method calls a [`MockCanBus`] has seen — used by tests to assert
12/// that group code calls only `send` and never `drain_inbound_nonblocking`.
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14pub enum MockRecordedCall {
15    /// `send(frame)` was invoked.
16    Send,
17    /// `drain_inbound_nonblocking()` was invoked.
18    Drain,
19}
20
21#[derive(Default)]
22struct Shared {
23    sent: Vec<CanFrame>,
24    pending_inbound: VecDeque<CanFrame>,
25    recorded: Vec<MockRecordedCall>,
26}
27
28/// Single-process mock CAN bus.
29///
30/// Construct via [`MockCanBus::new`] for a self-loopback (frames you send come
31/// straight back from `drain_inbound_nonblocking`), or via
32/// [`MockCanBus::pair`] for an A↔B loopback (frames sent on A appear on B's
33/// inbound and vice-versa).
34#[derive(Clone)]
35pub struct MockCanBus {
36    name: String,
37    caps: BusCapabilities,
38    me: Arc<Mutex<Shared>>,
39    peer: Option<Arc<Mutex<Shared>>>,
40}
41
42impl MockCanBus {
43    /// Classical-capability mock with self-loopback.
44    pub fn new(name: impl Into<String>) -> Self {
45        Self::with_capabilities(name, BusCapabilities::classical())
46    }
47
48    /// FD-capability mock with self-loopback — accepts and loops back FD frames
49    /// so the FD send/receive path is testable without an FD-capable interface.
50    pub fn new_fd(name: impl Into<String>) -> Self {
51        Self::with_capabilities(name, BusCapabilities::fd())
52    }
53
54    /// Mock with explicit capabilities (tests that need FD-flagged behavior).
55    pub fn with_capabilities(name: impl Into<String>, caps: BusCapabilities) -> Self {
56        Self {
57            name: name.into(),
58            caps,
59            me: Arc::new(Mutex::new(Shared::default())),
60            peer: None,
61        }
62    }
63
64    /// Pair of mocks that loopback to each other (`a.send(f)` → `b.drain` sees `f`).
65    pub fn pair(name_a: impl Into<String>, name_b: impl Into<String>) -> (Self, Self) {
66        let a_shared = Arc::new(Mutex::new(Shared::default()));
67        let b_shared = Arc::new(Mutex::new(Shared::default()));
68        let caps = BusCapabilities::classical();
69        let a = Self {
70            name: name_a.into(),
71            caps,
72            me: a_shared.clone(),
73            peer: Some(b_shared.clone()),
74        };
75        let b = Self {
76            name: name_b.into(),
77            caps,
78            me: b_shared,
79            peer: Some(a_shared),
80        };
81        (a, b)
82    }
83
84    /// Snapshot of every frame this bus has transmitted via `send`.
85    pub fn sent_frames(&self) -> Vec<CanFrame> {
86        self.me.lock().unwrap().sent.clone()
87    }
88
89    /// Snapshot of the call sequence for assertion of "sends never read".
90    pub fn recorded_calls(&self) -> Vec<MockRecordedCall> {
91        self.me.lock().unwrap().recorded.clone()
92    }
93
94    /// Push a frame onto this bus's inbound queue (as if a peer wrote it).
95    pub fn inject_frame(&self, frame: CanFrame) {
96        self.me.lock().unwrap().pending_inbound.push_back(frame);
97    }
98}
99
100impl CanBus for MockCanBus {
101    fn name(&self) -> &str {
102        &self.name
103    }
104
105    fn capabilities(&self) -> BusCapabilities {
106        self.caps
107    }
108
109    fn send(&mut self, frame: &CanFrame) -> Result<(), TransportError> {
110        // Capability validation (mirrors what SocketCanBus does).
111        if frame.is_fd() && !self.caps.supports_fd {
112            return Err(TransportError::FdFrameOnNonFdBus);
113        }
114        if frame.len > self.caps.max_payload_len {
115            return Err(TransportError::PayloadExceedsBusCapacity {
116                len: frame.len,
117                max: self.caps.max_payload_len,
118            });
119        }
120        {
121            let mut me = self.me.lock().unwrap();
122            me.sent.push(*frame);
123            me.recorded.push(MockRecordedCall::Send);
124        }
125        // Deliver to the peer (or to self if unpaired — self-loopback).
126        match &self.peer {
127            Some(p) => p.lock().unwrap().pending_inbound.push_back(*frame),
128            None => self.me.lock().unwrap().pending_inbound.push_back(*frame),
129        }
130        Ok(())
131    }
132
133    fn drain_inbound_nonblocking(&mut self) -> Result<Vec<CanFrame>, TransportError> {
134        let mut me = self.me.lock().unwrap();
135        me.recorded.push(MockRecordedCall::Drain);
136        Ok(me.pending_inbound.drain(..).collect())
137    }
138
139    fn raw_fd(&self) -> Option<RawFd> {
140        None
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn pair_loopback_delivers() {
150        let (mut a, mut b) = MockCanBus::pair("a", "b");
151        let f = CanFrame::classical(0x101, &[1, 2, 3]).unwrap();
152        a.send(&f).unwrap();
153        let got = b.drain_inbound_nonblocking().unwrap();
154        assert_eq!(got, vec![f]);
155        // Sender's own drain is empty (no echo to self in pair mode).
156        assert!(a.drain_inbound_nonblocking().unwrap().is_empty());
157    }
158
159    #[test]
160    fn self_loopback_delivers() {
161        let mut bus = MockCanBus::new("solo");
162        let f = CanFrame::classical(0x101, &[0xFF; 8]).unwrap();
163        bus.send(&f).unwrap();
164        let got = bus.drain_inbound_nonblocking().unwrap();
165        assert_eq!(got, vec![f]);
166    }
167
168    #[test]
169    fn inject_and_drain() {
170        let bus = MockCanBus::new("solo");
171        let f = CanFrame::classical(0x10, &[1; 8]).unwrap();
172        bus.inject_frame(f);
173        let mut bus = bus;
174        assert_eq!(bus.drain_inbound_nonblocking().unwrap(), vec![f]);
175    }
176
177    #[test]
178    fn sent_frames_records_in_order() {
179        let mut bus = MockCanBus::new("solo");
180        let f1 = CanFrame::classical(0x10, &[1; 8]).unwrap();
181        let f2 = CanFrame::classical(0x11, &[2; 8]).unwrap();
182        bus.send(&f1).unwrap();
183        bus.send(&f2).unwrap();
184        assert_eq!(bus.sent_frames(), vec![f1, f2]);
185    }
186
187    #[test]
188    fn fd_frame_rejected_on_classical_mock() {
189        let mut bus = MockCanBus::new("solo");
190        let f = CanFrame::fd(0x100, &[0; 16]).unwrap();
191        assert!(matches!(
192            bus.send(&f),
193            Err(TransportError::FdFrameOnNonFdBus)
194        ));
195    }
196
197    #[test]
198    fn fd_mock_accepts_fd_frames() {
199        let mut bus = MockCanBus::with_capabilities("fd", BusCapabilities::fd());
200        let f = CanFrame::fd(0x100, &[0; 16]).unwrap();
201        bus.send(&f).unwrap();
202        assert_eq!(bus.drain_inbound_nonblocking().unwrap(), vec![f]);
203    }
204
205    #[test]
206    fn new_fd_round_trips_fd_frame_preserving_flag_and_payload() {
207        let mut bus = MockCanBus::new_fd("fd0");
208        assert!(bus.capabilities().supports_fd);
209        let f = CanFrame::fd(0x123, &[0xAB; 24]).unwrap();
210        bus.send(&f).unwrap();
211        let got = bus.drain_inbound_nonblocking().unwrap();
212        assert_eq!(got.len(), 1);
213        assert!(got[0].is_fd());
214        assert_eq!(got[0].payload(), &[0xAB; 24]);
215    }
216
217    #[test]
218    fn recorded_calls_track_send_vs_drain() {
219        let mut bus = MockCanBus::new("solo");
220        let _ = bus.send(&CanFrame::classical(0x10, &[]).unwrap());
221        let _ = bus.drain_inbound_nonblocking();
222        let _ = bus.send(&CanFrame::classical(0x11, &[]).unwrap());
223        assert_eq!(
224            bus.recorded_calls(),
225            vec![
226                MockRecordedCall::Send,
227                MockRecordedCall::Drain,
228                MockRecordedCall::Send
229            ]
230        );
231    }
232
233    #[test]
234    fn raw_fd_is_none() {
235        let bus = MockCanBus::new("solo");
236        assert!(bus.raw_fd().is_none());
237    }
238}