1use std::collections::VecDeque;
10use std::io::{self, Read, Write};
11use std::sync::{Arc, Condvar, Mutex};
12use std::time::Duration;
13
14use super::handle::{
15 ExitStatus, ProcessCleanupReport, ProcessError, ProcessHandle, ProcessKiller, ProcessSpawner,
16 SpawnSpec, WaitOutcome,
17};
18
19#[derive(Clone, Debug)]
21pub struct MockProcessConfig {
22 pub pid: u32,
25 pub pgid: Option<u32>,
27 pub stdout: Vec<u8>,
29 pub stderr: Vec<u8>,
31 pub exit_status: Option<ExitStatus>,
35 pub force_timeout: bool,
39 pub spawn_error: Option<ProcessError>,
43 pub wait_error: Option<String>,
45 pub cleanup_report: Option<ProcessCleanupReport>,
47}
48
49impl Default for MockProcessConfig {
50 fn default() -> Self {
51 Self {
52 pid: 99_999,
53 pgid: Some(99_999),
54 stdout: Vec::new(),
55 stderr: Vec::new(),
56 exit_status: Some(ExitStatus::from_code(0)),
57 force_timeout: false,
58 spawn_error: None,
59 wait_error: None,
60 cleanup_report: None,
61 }
62 }
63}
64
65impl MockProcessConfig {
66 pub fn completed(exit_code: i32) -> Self {
69 Self {
70 exit_status: Some(ExitStatus::from_code(exit_code)),
71 ..Self::default()
72 }
73 }
74
75 pub fn with_stdout(exit_code: i32, stdout: impl Into<Vec<u8>>) -> Self {
78 Self {
79 stdout: stdout.into(),
80 exit_status: Some(ExitStatus::from_code(exit_code)),
81 ..Self::default()
82 }
83 }
84
85 pub fn running() -> Self {
89 Self {
90 exit_status: None,
91 ..Self::default()
92 }
93 }
94}
95
96#[derive(Default)]
97struct MockSpawnerInner {
98 queue: VecDeque<(MockProcessConfig, Arc<MockState>)>,
99 captured: Vec<SpawnSpec>,
100 last_controller: Option<MockHandleController>,
101}
102
103pub struct MockSpawner {
106 inner: Mutex<MockSpawnerInner>,
107}
108
109impl Default for MockSpawner {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl MockSpawner {
116 pub fn new() -> Self {
119 Self {
120 inner: Mutex::new(MockSpawnerInner::default()),
121 }
122 }
123
124 pub fn enqueue(&self, config: MockProcessConfig) -> MockHandleController {
129 let state = Arc::new(MockState::new(&config));
130 let controller = MockHandleController {
131 state: Arc::clone(&state),
132 };
133 let mut inner = self.inner.lock().expect("MockSpawner mutex poisoned");
134 inner.queue.push_back((config, state));
135 inner.last_controller = Some(controller.clone());
136 controller
137 }
138
139 pub fn captured(&self) -> Vec<SpawnSpec> {
141 self.inner
142 .lock()
143 .expect("MockSpawner mutex poisoned")
144 .captured
145 .clone()
146 }
147
148 pub fn last_controller(&self) -> Option<MockHandleController> {
151 self.inner
152 .lock()
153 .expect("MockSpawner mutex poisoned")
154 .last_controller
155 .clone()
156 }
157}
158
159impl ProcessSpawner for MockSpawner {
160 fn spawn(&self, spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError> {
161 let (config, state) = {
162 let mut inner = self.inner.lock().expect("MockSpawner mutex poisoned");
163 inner.captured.push(spec);
164 inner.queue.pop_front().expect(
165 "MockSpawner: spawn() called with no enqueued configuration. Call \
166 MockSpawner::enqueue(...) before each expected spawn.",
167 )
168 };
169
170 if let Some(err) = config.spawn_error {
171 return Err(err);
172 }
173
174 let killer: Arc<dyn ProcessKiller> = Arc::new(MockKiller {
175 pid: config.pid,
176 state: Arc::clone(&state),
177 });
178
179 Ok(Box::new(MockProcess {
180 pid: config.pid,
181 pgid: config.pgid,
182 killer,
183 state,
184 stdin_taken: false,
185 stdout_taken: false,
186 stderr_taken: false,
187 }))
188 }
189}
190
191#[derive(Clone)]
194pub struct MockHandleController {
195 state: Arc<MockState>,
196}
197
198impl MockHandleController {
199 pub fn append_stdout(&self, bytes: &[u8]) {
202 let mut data = self.state.stdout.lock().unwrap();
203 data.extend_from_slice(bytes);
204 self.state.stdout_cv.notify_all();
205 }
206
207 pub fn append_stderr(&self, bytes: &[u8]) {
209 let mut data = self.state.stderr.lock().unwrap();
210 data.extend_from_slice(bytes);
211 self.state.stderr_cv.notify_all();
212 }
213
214 pub fn complete_with(&self, status: ExitStatus) {
217 let mut exit = self.state.exit.lock().unwrap();
218 if exit.is_none() {
219 *exit = Some(ExitOutcome {
220 status,
221 killed: false,
222 });
223 }
224 drop(exit);
225 self.state.notify_exit_and_pipes();
226 }
227
228 pub fn was_killed(&self) -> bool {
230 self.state
231 .exit
232 .lock()
233 .unwrap()
234 .as_ref()
235 .map(|o| o.killed)
236 .unwrap_or(false)
237 }
238
239 pub fn stdin_written(&self) -> Vec<u8> {
242 self.state.stdin_written.lock().unwrap().clone()
243 }
244}
245
246struct MockState {
247 stdout: Mutex<Vec<u8>>,
249 stderr: Mutex<Vec<u8>>,
251 stdin_written: Mutex<Vec<u8>>,
253 exit: Mutex<Option<ExitOutcome>>,
255 exit_cv: Condvar,
256 stdout_cv: Condvar,
257 stderr_cv: Condvar,
258 force_timeout: bool,
260 wait_error: Option<String>,
261 cleanup_report: Option<ProcessCleanupReport>,
262}
263
264#[derive(Clone, Copy, Debug)]
265struct ExitOutcome {
266 status: ExitStatus,
267 killed: bool,
268}
269
270impl MockState {
271 fn new(config: &MockProcessConfig) -> Self {
272 let exit = config.exit_status.map(|status| ExitOutcome {
273 status,
274 killed: false,
275 });
276 Self {
277 stdout: Mutex::new(config.stdout.clone()),
278 stderr: Mutex::new(config.stderr.clone()),
279 stdin_written: Mutex::new(Vec::new()),
280 exit: Mutex::new(exit),
281 exit_cv: Condvar::new(),
282 stdout_cv: Condvar::new(),
283 stderr_cv: Condvar::new(),
284 force_timeout: config.force_timeout,
285 wait_error: config.wait_error.clone(),
286 cleanup_report: config.cleanup_report.clone(),
287 }
288 }
289
290 fn is_exited(&self) -> bool {
291 self.exit.lock().unwrap().is_some()
292 }
293
294 fn wait_for_exit(&self, timeout: Option<Duration>) -> Option<ExitOutcome> {
295 let mut exit = self.exit.lock().unwrap();
296 if let Some(timeout) = timeout {
297 if exit.is_none() {
298 let (next, result) = self.exit_cv.wait_timeout(exit, timeout).unwrap();
299 exit = next;
300 if result.timed_out() && exit.is_none() {
301 return None;
302 }
303 }
304 } else {
305 while exit.is_none() {
306 exit = self.exit_cv.wait(exit).unwrap();
307 }
308 }
309 *exit
310 }
311
312 fn record_kill(&self) {
313 let mut exit = self.exit.lock().unwrap();
314 if exit.is_none() {
315 *exit = Some(ExitOutcome {
316 status: ExitStatus::from_signal(9),
317 killed: true,
318 });
319 } else if let Some(outcome) = exit.as_mut() {
320 outcome.killed = true;
321 }
322 drop(exit);
323 self.notify_exit_and_pipes();
324 }
325
326 fn notify_exit_and_pipes(&self) {
327 self.exit_cv.notify_all();
328
329 {
333 let _stdout = self.stdout.lock().unwrap();
334 self.stdout_cv.notify_all();
335 }
336 {
337 let _stderr = self.stderr.lock().unwrap();
338 self.stderr_cv.notify_all();
339 }
340 }
341
342 fn cleanup_report(&self, root_pid: u32, signal: i32) -> ProcessCleanupReport {
343 self.cleanup_report
344 .clone()
345 .unwrap_or_else(|| ProcessCleanupReport::for_signal(Some(root_pid), signal))
346 }
347}
348
349pub struct MockProcess {
351 pid: u32,
352 pgid: Option<u32>,
353 killer: Arc<dyn ProcessKiller>,
354 state: Arc<MockState>,
355 stdin_taken: bool,
356 stdout_taken: bool,
357 stderr_taken: bool,
358}
359
360impl ProcessHandle for MockProcess {
361 fn pid(&self) -> Option<u32> {
362 Some(self.pid)
363 }
364
365 fn process_group_id(&self) -> Option<u32> {
366 self.pgid
367 }
368
369 fn killer(&self) -> Arc<dyn ProcessKiller> {
370 Arc::clone(&self.killer)
371 }
372
373 fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
374 if self.stdin_taken {
375 return None;
376 }
377 self.stdin_taken = true;
378 Some(Box::new(MockStdin {
379 state: Arc::clone(&self.state),
380 }))
381 }
382
383 fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
384 if self.stdout_taken {
385 return None;
386 }
387 self.stdout_taken = true;
388 Some(Box::new(MockStdoutReader {
389 state: Arc::clone(&self.state),
390 kind: PipeKind::Stdout,
391 }))
392 }
393
394 fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
395 if self.stderr_taken {
396 return None;
397 }
398 self.stderr_taken = true;
399 Some(Box::new(MockStdoutReader {
400 state: Arc::clone(&self.state),
401 kind: PipeKind::Stderr,
402 }))
403 }
404
405 fn wait_with_timeout(
406 &mut self,
407 timeout: Option<Duration>,
408 interrupt: &dyn Fn() -> bool,
409 ) -> io::Result<WaitOutcome> {
410 if let Some(error) = self.state.wait_error.as_ref() {
411 return Err(io::Error::other(error.clone()));
412 }
413 if self.state.force_timeout {
414 self.state.record_kill();
415 return Ok(WaitOutcome::TimedOut(
416 self.state.cleanup_report(self.pid, 9),
417 ));
418 }
419 let deadline = timeout.map(|timeout| std::time::Instant::now() + timeout);
424 loop {
425 if let Some(outcome) = self.state.wait_for_exit(Some(Duration::from_millis(5))) {
426 return Ok(WaitOutcome::Exited(outcome.status));
427 }
428 if interrupt() {
429 self.state.record_kill();
430 return Ok(WaitOutcome::Interrupted(
431 self.state.cleanup_report(self.pid, 15),
432 ));
433 }
434 if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
435 self.state.record_kill();
436 return Ok(WaitOutcome::TimedOut(
437 self.state.cleanup_report(self.pid, 9),
438 ));
439 }
440 }
441 }
442
443 fn wait(&mut self) -> io::Result<ExitStatus> {
444 if let Some(error) = self.state.wait_error.as_ref() {
445 return Err(io::Error::other(error.clone()));
446 }
447 let outcome = self
448 .state
449 .wait_for_exit(None)
450 .expect("wait without timeout returned None");
451 Ok(outcome.status)
452 }
453}
454
455struct MockStdin {
456 state: Arc<MockState>,
457}
458
459impl Write for MockStdin {
460 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
461 self.state
462 .stdin_written
463 .lock()
464 .unwrap()
465 .extend_from_slice(buf);
466 Ok(buf.len())
467 }
468
469 fn flush(&mut self) -> io::Result<()> {
470 Ok(())
471 }
472}
473
474#[derive(Clone, Copy)]
475enum PipeKind {
476 Stdout,
477 Stderr,
478}
479
480struct MockStdoutReader {
481 state: Arc<MockState>,
482 kind: PipeKind,
483}
484
485impl MockStdoutReader {
486 fn pipe_lock(&self) -> &Mutex<Vec<u8>> {
487 match self.kind {
488 PipeKind::Stdout => &self.state.stdout,
489 PipeKind::Stderr => &self.state.stderr,
490 }
491 }
492
493 fn pipe_cv(&self) -> &Condvar {
494 match self.kind {
495 PipeKind::Stdout => &self.state.stdout_cv,
496 PipeKind::Stderr => &self.state.stderr_cv,
497 }
498 }
499}
500
501impl Read for MockStdoutReader {
502 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
503 let lock = self.pipe_lock();
504 let cv = self.pipe_cv();
505 let mut data = lock.lock().unwrap();
506 loop {
507 if !data.is_empty() {
508 let n = data.len().min(buf.len());
509 buf[..n].copy_from_slice(&data[..n]);
510 data.drain(..n);
511 return Ok(n);
512 }
513 if self.state.is_exited() {
516 return Ok(0);
517 }
518 data = cv.wait(data).unwrap();
519 }
520 }
521}
522
523struct MockKiller {
524 pid: u32,
525 state: Arc<MockState>,
526}
527
528impl ProcessKiller for MockKiller {
529 fn kill(&self) -> ProcessCleanupReport {
530 self.state.record_kill();
531 self.state.cleanup_report(self.pid, 9)
532 }
533}