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