//! Test Safety Utilities //! //! Provides safe testing patterns to eliminate unwrap/expect usage in tests //! and improve debugging experience with better error messages. use std::fmt::Debug; use std::future::Future; use std::time::Duration; use tokio::time::timeout; /// Result type for test operations with descriptive context pub type TestResult = Result; /// Comprehensive test error type with context #[derive(Debug, thiserror::Error)] pub enum TestError { #[error("Test setup failed: {context}")] SetupFailed { context: String }, #[error("Test assertion failed: {context}")] AssertionFailed { context: String }, #[error("Test timeout after {duration:?}: {context}")] Timeout { duration: Duration, context: String }, #[error("Resource cleanup failed: {context}")] CleanupFailed { context: String }, #[error("External service unavailable: {service} - {reason}")] ServiceUnavailable { service: String, reason: String }, #[error("Test data corruption: {context}")] DataCorruption { context: String }, #[error("Network error in test: {context}")] NetworkError { context: String }, #[error("Database error in test: {context}")] DatabaseError { context: String }, #[error("Generic test failure: {context}")] Generic { context: String }, } impl TestError { pub fn setup(context: impl Into) -> Self { Self::SetupFailed { context: context.into(), } } pub fn assertion(context: impl Into) -> Self { Self::AssertionFailed { context: context.into(), } } pub fn timeout(duration: Duration, context: impl Into) -> Self { Self::Timeout { duration, context: context.into(), } } pub fn cleanup(context: impl Into) -> Self { Self::CleanupFailed { context: context.into(), } } pub fn service_unavailable(service: impl Into, reason: impl Into) -> Self { Self::ServiceUnavailable { service: service.into(), reason: reason.into(), } } pub fn data_corruption(context: impl Into) -> Self { Self::DataCorruption { context: context.into(), } } pub fn network(context: impl Into) -> Self { Self::NetworkError { context: context.into(), } } pub fn database(context: impl Into) -> Self { Self::DatabaseError { context: context.into(), } } pub fn generic(context: impl Into) -> Self { Self::Generic { context: context.into(), } } } /// Safe assertion macro with descriptive error messages #[macro_export] macro_rules! test_assert { ($condition:expr, $context:expr) => { if !$condition { return Err(TestError::assertion(format!("{}: condition failed: {}", $context, stringify!($condition)))); } }; ($condition:expr, $context:expr, $($arg:tt)*) => { if !$condition { return Err(TestError::assertion(format!("{}: {}", $context, format!($($arg)*)))); } }; } /// Safe equality assertion with descriptive error messages #[macro_export] macro_rules! test_assert_eq { ($left:expr, $right:expr, $context:expr) => { let left_val = $left; let right_val = $right; if left_val != right_val { return Err(TestError::assertion(format!( "{}: assertion failed: {} == {}\nleft: {:?}\nright: {:?}", $context, stringify!($left), stringify!($right), left_val, right_val ))); } }; } /// Safe unwrap with descriptive error context pub trait SafeTestUnwrap { fn safe_unwrap(self, context: &str) -> TestResult; } impl SafeTestUnwrap for Result { fn safe_unwrap(self, context: &str) -> TestResult { self.map_err(|e| TestError::generic(format!("{}: {:?}", context, e))) } } impl SafeTestUnwrap for Option { fn safe_unwrap(self, context: &str) -> TestResult { self.ok_or_else(|| TestError::generic(format!("{}: Option was None", context))) } } /// Safe timeout wrapper for async operations pub async fn with_test_timeout(future: F, duration: Duration, context: &str) -> TestResult where F: Future, { timeout(duration, future) .await .map_err(|_| TestError::timeout(duration, context)) } /// Safe timeout wrapper for Result returning async operations pub async fn with_test_timeout_result( future: F, duration: Duration, context: &str, ) -> TestResult where F: Future>, E: Debug, { let result = timeout(duration, future) .await .map_err(|_| TestError::timeout(duration, context))?; result.map_err(|e| TestError::generic(format!("{}: {:?}", context, e))) } /// Test fixture with automatic cleanup pub struct TestFixture { resource: Option, cleanup_fn: Option TestResult<()> + Send>>, } impl std::fmt::Debug for TestFixture { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TestFixture") .field("resource", &self.resource) .field( "cleanup_fn", &if self.cleanup_fn.is_some() { "" } else { "" }, ) .finish() } } impl TestFixture { pub fn new(resource: T) -> Self { Self { resource: Some(resource), cleanup_fn: None, } } pub fn with_cleanup(mut self, cleanup_fn: F) -> Self where F: FnOnce(T) -> TestResult<()> + Send + 'static, { self.cleanup_fn = Some(Box::new(cleanup_fn)); self } pub fn get(&self) -> &T { self.resource.as_ref().expect("Resource already taken") } pub fn get_mut(&mut self) -> &mut T { self.resource.as_mut().expect("Resource already taken") } } impl Drop for TestFixture { fn drop(&mut self) { if let (Some(resource), Some(cleanup_fn)) = (self.resource.take(), self.cleanup_fn.take()) { if let Err(e) = cleanup_fn(resource) { eprintln!("Test cleanup failed: {}", e); } } } } /// Property based testing utilities pub mod property { use super::*; /// Run property test with proper error handling pub fn run_property_test(test_name: &str, iterations: usize, test_fn: F) -> TestResult<()> where F: Fn(usize) -> TestResult<()>, { for i in 0..iterations { test_fn(i).map_err(|e| { TestError::assertion(format!("{} failed at iteration {}: {}", test_name, i, e)) })?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_safe_unwrap_success() -> TestResult<()> { let result: Result = Ok(42); let value = result.safe_unwrap("test operation")?; test_assert_eq!(value, 42, "safe_unwrap should return correct value"); Ok(()) } #[tokio::test] async fn test_safe_unwrap_failure() { let result: Result = Err("test error"); match result.safe_unwrap("test operation") { Err(TestError::Generic { context }) => { assert!(context.contains("test operation")); assert!(context.contains("test error")); }, _ => panic!("Expected TestError::Generic"), } } #[tokio::test] async fn test_timeout_wrapper() { let result = with_test_timeout( async { tokio::time::sleep(Duration::from_millis(100)).await }, Duration::from_millis(50), "timeout test", ) .await; match result { Err(TestError::Timeout { duration, context }) => { assert_eq!(duration, Duration::from_millis(50)); assert_eq!(context, "timeout test"); }, _ => panic!("Expected timeout error"), } } #[tokio::test] async fn test_property_testing() -> TestResult<()> { property::run_property_test("simple addition", 10, |i| { let x = i as i32; let y = x + 1; test_assert!(y > x, "addition should increase value"); Ok(()) }) } }