use crossbeam::channel::unbounded;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
pub default_timeout: Duration,
pub max_timeout: Duration,
pub enable_graceful_shutdown: bool,
pub grace_period: Duration,
}
impl Default for TimeoutConfig {
fn default() -> Self {
TimeoutConfig {
default_timeout: Duration::from_secs(5),
max_timeout: Duration::from_secs(300), enable_graceful_shutdown: true,
grace_period: Duration::from_millis(100),
}
}
}
pub struct TestTimeout {
config: TimeoutConfig,
}
impl TestTimeout {
pub fn new(config: TimeoutConfig) -> Self {
TestTimeout { config }
}
pub fn default() -> Self {
Self::new(TimeoutConfig::default())
}
pub fn run_with_timeout<F, T>(&self, timeout: Duration, func: F) -> Result<T, TimeoutError>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
let timeout: _ = self.validate_timeout(timeout)?;
let (sender, receiver) = unbounded::<Result<T, TimeoutError>>();
let handle: _ = std::thread::spawn(move || {
let result: _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
match result {
Ok(value) => sender.send(Ok(value)).unwrap(),
Err(_) => sender.send(Err(TimeoutError::Panicked)).unwrap(),
}
});
match receiver.recv_timeout(timeout) {
Ok(result) => {
let _: _ = handle.join();
result
}
Err(_) => {
Err(TimeoutError::Exceeded(timeout))
}
}
}
pub fn run_async_with_timeout<F, T>(
&self,
timeout: Duration,
func: F,
) -> Result<T, TimeoutError>
where
F: FnOnce() -> T + Send + 'static,
F: std::panic::UnwindSafe,
T: Send + 'static,
{
let timeout: _ = self.validate_timeout(timeout)?;
let (sender, receiver) = unbounded::<Result<T, TimeoutError>>();
let handle = std::thread::spawn(move || {
let result = std::panic::catch_unwind(func).map_err(|_| TimeoutError::Panicked);
let _ = sender.send(result);
});
match receiver.recv_timeout(timeout) {
Ok(result) => {
let _ = handle.join();
result
}
Err(_) => Err(TimeoutError::Exceeded(timeout)),
}
}
fn validate_timeout(&self, timeout: Duration) -> Result<Duration, TimeoutError> {
if timeout == Duration::from_secs(0) {
return Ok(self.config.default_timeout);
}
if timeout > self.config.max_timeout {
return Err(TimeoutError::Invalid(
"timeout exceeds maximum allowed".to_string(),
));
}
Ok(timeout)
}
}
#[derive(Debug, Clone)]
pub enum TimeoutError {
Exceeded(Duration),
Panicked,
Invalid(String),
}
impl std::fmt::Display for TimeoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeoutError::Exceeded(timeout) => {
write!(f, "Test execution exceeded timeout of {:?}", timeout)
}
TimeoutError::Panicked => write!(f, "Test execution panicked"),
TimeoutError::Invalid(msg) => write!(f, "Invalid timeout: {}", msg),
}
}
}
impl std::error::Error for TimeoutError {}
pub struct TimeoutContext {
start_time: Instant,
timeout: Duration,
active_tests: Arc<Mutex<Vec<TestTimeoutHandle>>>,
}
struct TestTimeoutHandle {
test_name: String,
}
impl TimeoutContext {
pub fn new(timeout: Duration) -> Self {
TimeoutContext {
start_time: Instant::now(),
timeout,
active_tests: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn start_test(&self, test_name: &str) -> TestTimeoutGuard {
let handle: _ = TestTimeoutHandle {
test_name: test_name.to_string(),
};
{
let mut active = self.active_tests.lock().unwrap();
active.push(handle);
}
TestTimeoutGuard {
test_name: test_name.to_string(),
active_tests: Arc::clone(&self.active_tests),
}
}
pub fn check_elapsed(&self) -> bool {
self.start_time.elapsed() > self.timeout
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
}
pub struct TestTimeoutGuard {
test_name: String,
active_tests: Arc<Mutex<Vec<TestTimeoutHandle>>>,
}
impl Drop for TestTimeoutGuard {
fn drop(&mut self) {
let mut active = self.active_tests.lock().unwrap();
active.retain(|h| h.test_name != self.test_name);
}
}
#[cfg(test)]
mod tests {
use super::TestTimeout;
use std::time::Duration;
#[test]
fn test_timeout_creation() {
let timeout: _ = TestTimeout::default();
assert!(timeout.config.default_timeout > Duration::from_secs(0));
}
#[test]
fn test_run_with_timeout_success() {
let timeout: _ = TestTimeout::default();
let result: _ = timeout.run_with_timeout(Duration::from_secs(1), || 42);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_run_with_timeout_failure() {
let timeout: _ = TestTimeout::default();
let result: _ = timeout.run_with_timeout(Duration::from_millis(10), || {
std::thread::sleep(Duration::from_millis(100));
42
});
assert!(result.is_err());
}
#[test]
fn run_async_with_timeout_returns_exceeded_before_blocking_closure_finishes() {
let timeout = TestTimeout::default();
let start = std::time::Instant::now();
let result = timeout.run_async_with_timeout(Duration::from_millis(50), || {
std::thread::sleep(Duration::from_secs(2));
42
});
assert!(matches!(result, Err(super::TimeoutError::Exceeded(_))));
assert!(
start.elapsed() < Duration::from_millis(800),
"timeout should return before the blocking closure finishes"
);
}
}