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}