Skip to main content

rust_supervisor/
exit_handler.rs

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
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10/// Abstraction for process exit, allowing tests to observe exit requests
11/// without actually terminating the process.
12pub 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)]
19pub struct DefaultExitHandler;
20
21impl ExitHandler for DefaultExitHandler {
22    /// Terminates the current process with the provided code.
23    fn exit(&self, code: i32) {
24        std::process::exit(code);
25    }
26}
27
28/// Test-friendly exit handler that records a flag instead of terminating.
29#[derive(Debug, Clone)]
30pub struct TestExitHandler {
31    /// Set to `true` when `exit()` was called.
32    pub called: Arc<AtomicBool>,
33    /// Captured exit code.
34    pub exit_code: Arc<std::sync::Mutex<Option<i32>>>,
35}
36
37impl TestExitHandler {
38    /// Creates a new test exit handler with `called = false`.
39    pub fn new() -> Self {
40        Self {
41            called: Arc::new(AtomicBool::new(false)),
42            exit_code: Arc::new(std::sync::Mutex::new(None)),
43        }
44    }
45
46    /// Returns `true` when `exit()` was called since the last reset.
47    pub fn was_called(&self) -> bool {
48        self.called.load(Ordering::SeqCst)
49    }
50
51    /// Returns the exit code if `exit()` was called.
52    pub fn last_exit_code(&self) -> Option<i32> {
53        *self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
54    }
55
56    /// Resets the recorded state.
57    pub fn reset(&self) {
58        self.called.store(false, Ordering::SeqCst);
59        *self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) = None;
60    }
61}
62
63impl ExitHandler for TestExitHandler {
64    /// Records the requested exit code without terminating the process.
65    fn exit(&self, code: i32) {
66        self.called.store(true, Ordering::SeqCst);
67        *self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) = Some(code);
68        // Do NOT call std::process::exit — let the test continue.
69    }
70}
71
72impl Default for TestExitHandler {
73    /// Creates a default test exit handler.
74    fn default() -> Self {
75        Self::new()
76    }
77}