|
| 1 | +//! Exit handler abstraction for testable process termination. |
| 2 | +//! |
| 3 | +//! The default implementation calls `std::process::exit(1)` as a last resort |
| 4 | +//! when orphaned tasks exceed the configured threshold. Tests can swap in a |
| 5 | +//! stub that records the exit request instead of terminating the process. |
| 6 | +
|
| 7 | +use std::sync::Arc; |
| 8 | +use std::sync::atomic::{AtomicBool, Ordering}; |
| 9 | + |
| 10 | +/// Abstraction for process exit, allowing tests to observe exit requests |
| 11 | +/// without actually terminating the process. |
| 12 | +pub trait ExitHandler: Send + Sync + std::fmt::Debug { |
| 13 | + /// Terminates the process (or records the request in test mode). |
| 14 | + fn exit(&self, code: i32); |
| 15 | +} |
| 16 | + |
| 17 | +/// Default exit handler that calls `std::process::exit(code)`. |
| 18 | +#[derive(Debug, Clone, Copy)] |
| 19 | +pub struct DefaultExitHandler; |
| 20 | + |
| 21 | +impl ExitHandler for DefaultExitHandler { |
| 22 | + fn exit(&self, code: i32) { |
| 23 | + std::process::exit(code); |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +/// Test-friendly exit handler that records a flag instead of terminating. |
| 28 | +#[derive(Debug, Clone)] |
| 29 | +pub struct TestExitHandler { |
| 30 | + /// Set to `true` when `exit()` was called. |
| 31 | + pub called: Arc<AtomicBool>, |
| 32 | + /// Captured exit code. |
| 33 | + pub exit_code: Arc<std::sync::Mutex<Option<i32>>>, |
| 34 | +} |
| 35 | + |
| 36 | +impl TestExitHandler { |
| 37 | + /// Creates a new test exit handler with `called = false`. |
| 38 | + pub fn new() -> Self { |
| 39 | + Self { |
| 40 | + called: Arc::new(AtomicBool::new(false)), |
| 41 | + exit_code: Arc::new(std::sync::Mutex::new(None)), |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + /// Returns `true` when `exit()` was called since the last reset. |
| 46 | + pub fn was_called(&self) -> bool { |
| 47 | + self.called.load(Ordering::SeqCst) |
| 48 | + } |
| 49 | + |
| 50 | + /// Returns the exit code if `exit()` was called. |
| 51 | + pub fn last_exit_code(&self) -> Option<i32> { |
| 52 | + *self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) |
| 53 | + } |
| 54 | + |
| 55 | + /// Resets the recorded state. |
| 56 | + pub fn reset(&self) { |
| 57 | + self.called.store(false, Ordering::SeqCst); |
| 58 | + *self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) = None; |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +impl ExitHandler for TestExitHandler { |
| 63 | + fn exit(&self, code: i32) { |
| 64 | + self.called.store(true, Ordering::SeqCst); |
| 65 | + *self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) = Some(code); |
| 66 | + // Do NOT call std::process::exit — let the test continue. |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +impl Default for TestExitHandler { |
| 71 | + fn default() -> Self { |
| 72 | + Self::new() |
| 73 | + } |
| 74 | +} |
0 commit comments