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