ailloli_ui_terminal_pty/
mock.rs

1//! Deterministic in-memory backend for tests, demos, and consumer simulations.
2
3use std::collections::VecDeque;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex};
6
7use crate::handle::{PtyBackend, PtySession};
8use crate::{PtyError, PtyEvent, PtyHandle, PtySize, PtySpawnConfig};
9
10/// Cloneable mock backend with one shared recording state.
11///
12/// Every clone and every session spawned from the same backend shares spawn
13/// configs, writes, resizes, and one global event queue. Session shutdown flags
14/// are per spawn, but [`PtyHandle`] clones share their session.
15/// No automatic output or exit event is generated.
16///
17/// # Panics
18///
19/// Public mock operations panic with `"mock pty state"` if shared state's mutex
20/// has been poisoned by another thread.
21///
22/// # Examples
23///
24/// ```
25/// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
26/// let backend = MockPtyBackend::default();
27/// backend.spawn(PtySpawnConfig::default()).unwrap().write(b"x").unwrap();
28/// assert_eq!(backend.writes(), vec![b"x".to_vec()]);
29/// ```
30#[derive(Clone, Default)]
31pub struct MockPtyBackend {
32    /// Recording state shared by backend and session clones.
33    state: Arc<Mutex<MockPtyState>>,
34}
35
36/// Shared configurable error, recordings, and injected event queue.
37#[derive(Default)]
38struct MockPtyState {
39    /// Error cloned and returned by every spawn, or `None` for success.
40    spawn_error: Option<PtyError>,
41    /// Successful spawn configurations in call order.
42    spawned_configs: Vec<PtySpawnConfig>,
43    /// Successful session write payloads in lock-acquisition order.
44    writes: Vec<Vec<u8>>,
45    /// Successful session resize values in lock-acquisition order.
46    resizes: Vec<PtySize>,
47    /// Globally injected events awaiting the next drain from any session.
48    events: VecDeque<PtyEvent>,
49}
50
51impl MockPtyBackend {
52    /// Configures a cloned error to be returned by every later spawn.
53    ///
54    /// The method consumes and returns this backend value, but mutates state
55    /// shared with any pre-existing clones. There is no setter to clear the error;
56    /// create a fresh default backend for successful spawning. Failed configs are
57    /// not added to [`Self::spawned_configs`].
58    ///
59    /// # Panics
60    ///
61    /// Panics if the shared mock mutex is poisoned.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtyError, PtySpawnConfig};
67    /// let backend = MockPtyBackend::default().with_spawn_error(PtyError::Spawn("fixture".into()));
68    /// assert_eq!(backend.spawn(PtySpawnConfig::default()).unwrap_err(), PtyError::Spawn("fixture".into()));
69    /// assert!(backend.spawned_configs().is_empty());
70    /// ```
71    pub fn with_spawn_error(self, error: PtyError) -> Self {
72        self.state.lock().expect("mock pty state").spawn_error = Some(error);
73        self
74    }
75
76    /// Appends an event to the global queue without requiring a spawned session.
77    ///
78    /// Events are stored verbatim and drained FIFO by the first session handle
79    /// that calls `drain_events`.
80    ///
81    /// # Panics
82    ///
83    /// Panics if the shared mock mutex is poisoned.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtyEvent, PtySpawnConfig};
89    /// let backend = MockPtyBackend::default(); backend.push_event(PtyEvent::Output(vec![1, 2]));
90    /// let handle = backend.spawn(PtySpawnConfig::default()).unwrap();
91    /// assert_eq!(handle.drain_events(), vec![PtyEvent::Output(vec![1, 2])]);
92    /// ```
93    pub fn push_event(&self, event: PtyEvent) {
94        self.state
95            .lock()
96            .expect("mock pty state")
97            .events
98            .push_back(event);
99    }
100
101    /// Clones successful spawn configurations in call order.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the shared mock mutex is poisoned.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
111    /// let backend = MockPtyBackend::default(); let config = PtySpawnConfig::default();
112    /// backend.spawn(config.clone()).unwrap();
113    /// assert_eq!(backend.spawned_configs(), vec![config]);
114    /// ```
115    pub fn spawned_configs(&self) -> Vec<PtySpawnConfig> {
116        self.state
117            .lock()
118            .expect("mock pty state")
119            .spawned_configs
120            .clone()
121    }
122
123    /// Clones all successful writes from every shared session.
124    ///
125    /// Empty writes are recorded. Mutating the returned vectors does not change
126    /// the backend.
127    ///
128    /// # Panics
129    ///
130    /// Panics if the shared mock mutex is poisoned.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
136    /// let backend = MockPtyBackend::default();
137    /// backend.spawn(PtySpawnConfig::default()).unwrap().write(&[]).unwrap();
138    /// assert_eq!(backend.writes(), vec![Vec::<u8>::new()]);
139    /// ```
140    pub fn writes(&self) -> Vec<Vec<u8>> {
141        self.state.lock().expect("mock pty state").writes.clone()
142    }
143
144    /// Clones all successful resize requests from every shared session.
145    ///
146    /// Values are recorded exactly; constructor invariants are not rechecked.
147    ///
148    /// # Panics
149    ///
150    /// Panics if the shared mock mutex is poisoned.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySize, PtySpawnConfig};
156    /// let backend = MockPtyBackend::default(); let size = PtySize::new(30, 90, 0, 0);
157    /// backend.spawn(PtySpawnConfig::default()).unwrap().resize(size).unwrap();
158    /// assert_eq!(backend.resizes(), vec![size]);
159    /// ```
160    pub fn resizes(&self) -> Vec<PtySize> {
161        self.state.lock().expect("mock pty state").resizes.clone()
162    }
163}
164
165impl PtyBackend for MockPtyBackend {
166    /// Returns the configured error or records config and creates a fresh session.
167    ///
168    /// # Errors
169    ///
170    /// Returns the error installed through [`Self::with_spawn_error`], when one
171    /// is configured. No session or spawn record is created in that case.
172    ///
173    /// # Panics
174    ///
175    /// Panics if the shared mock-state mutex is poisoned.
176    fn spawn(&self, config: PtySpawnConfig) -> Result<PtyHandle, PtyError> {
177        let mut state = self.state.lock().expect("mock pty state");
178        if let Some(error) = state.spawn_error.clone() {
179            return Err(error);
180        }
181        state.spawned_configs.push(config);
182        drop(state);
183        Ok(PtyHandle::new(Arc::new(MockPtySession {
184            state: self.state.clone(),
185            shutdown: AtomicBool::new(false),
186        })))
187    }
188}
189
190/// Per-spawn shutdown state connected to the backend-global recordings/queue.
191struct MockPtySession {
192    /// Shared backend recording state.
193    state: Arc<Mutex<MockPtyState>>,
194    /// Per-session sequentially consistent shutdown flag.
195    shutdown: AtomicBool,
196}
197
198impl PtySession for MockPtySession {
199    /// Records an owned byte copy unless this session is closed.
200    ///
201    /// # Errors
202    ///
203    /// Returns [`PtyError::Closed`] after this session has shut down.
204    ///
205    /// # Panics
206    ///
207    /// Panics if the shared mock-state mutex is poisoned.
208    fn write(&self, bytes: &[u8]) -> Result<(), PtyError> {
209        if self.shutdown.load(Ordering::SeqCst) {
210            return Err(PtyError::Closed);
211        }
212        self.state
213            .lock()
214            .expect("mock pty state")
215            .writes
216            .push(bytes.to_vec());
217        Ok(())
218    }
219
220    /// Records the exact dimensions unless this session is closed.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`PtyError::Closed`] after this session has shut down.
225    ///
226    /// # Panics
227    ///
228    /// Panics if the shared mock-state mutex is poisoned.
229    fn resize(&self, size: PtySize) -> Result<(), PtyError> {
230        if self.shutdown.load(Ordering::SeqCst) {
231            return Err(PtyError::Closed);
232        }
233        self.state
234            .lock()
235            .expect("mock pty state")
236            .resizes
237            .push(size);
238        Ok(())
239    }
240
241    /// Drains the backend-global FIFO even after this session shuts down.
242    fn drain_events(&self) -> Vec<PtyEvent> {
243        self.state
244            .lock()
245            .expect("mock pty state")
246            .events
247            .drain(..)
248            .collect()
249    }
250
251    /// Sets this session's shutdown flag; repeated calls succeed.
252    ///
253    /// # Errors
254    ///
255    /// This mock implementation is infallible and never returns an error.
256    fn shutdown(&self) -> Result<(), PtyError> {
257        self.shutdown.store(true, Ordering::SeqCst);
258        Ok(())
259    }
260
261    /// Loads this session's sequentially consistent shutdown flag.
262    fn is_shutdown(&self) -> bool {
263        self.shutdown.load(Ordering::SeqCst)
264    }
265}