Skip to main content

reinhardt_testkit/
resource.rs

1//! Test resource management with automatic setup and teardown
2//!
3//! This module provides traits and helpers for managing test resources
4//! with automatic cleanup, similar to pytest fixtures or JUnit's BeforeEach/AfterEach.
5//!
6//! ## Overview
7//!
8//! - `TestResource`: Per-test setup/teardown (BeforeEach/AfterEach pattern)
9//! - `TeardownGuard`: RAII guard for automatic resource cleanup
10//! - `SuiteResource`: Suite-wide shared resources (BeforeAll/AfterAll pattern)
11//! - `SuiteGuard`: Reference-counted guard with automatic cleanup when last user drops
12//!
13//! ## Examples
14//!
15//! ### Per-test resource (BeforeEach/AfterEach)
16//!
17//! ```rust
18//! use reinhardt_testkit::resource::{TestResource, TeardownGuard};
19//! use rstest::*;
20//!
21//! struct TestEnv {
22//!     temp_dir: std::path::PathBuf,
23//! }
24//!
25//! impl TestResource for TestEnv {
26//!     fn setup() -> Self {
27//!         let temp = tempfile::tempdir().unwrap();
28//!         Self { temp_dir: temp.path().to_path_buf() }
29//!     }
30//!
31//!     fn teardown(&mut self) {
32//!         // Cleanup code here
33//!         let _ = std::fs::remove_dir_all(&self.temp_dir);
34//!     }
35//! }
36//!
37//! #[fixture]
38//! fn ctx() -> TeardownGuard<TestEnv> {
39//!     TeardownGuard::new()
40//! }
41//!
42//! #[rstest]
43//! fn test_something(ctx: TeardownGuard<TestEnv>) {
44//!     // ctx.temp_dir is available
45//!     // teardown() is automatically called when ctx goes out of scope
46//! }
47//! ```
48//!
49//! ### Suite-wide resource (BeforeAll/AfterAll)
50//!
51//! ```rust,no_run
52//! use reinhardt_testkit::resource::{SuiteResource, SuiteGuard, acquire_suite};
53//! use rstest::*;
54//! use std::sync::{OnceLock, Mutex, Weak};
55//!
56//! struct DatabaseSuite {
57//!     connection_string: String,
58//! }
59//!
60//! impl SuiteResource for DatabaseSuite {
61//!     fn init() -> Self {
62//!         // Expensive setup (e.g., start test database)
63//!         Self { connection_string: "test_db".to_string() }
64//!     }
65//! }
66//!
67//! impl Drop for DatabaseSuite {
68//!     fn drop(&mut self) {
69//!         // Cleanup when last test completes
70//!         println!("Dropping suite resource");
71//!     }
72//! }
73//!
74//! static SUITE: OnceLock<Mutex<Weak<DatabaseSuite>>> = OnceLock::new();
75//!
76//! #[fixture]
77//! fn suite() -> SuiteGuard<DatabaseSuite> {
78//!     acquire_suite(&SUITE)
79//! }
80//!
81//! #[rstest]
82//! fn test_with_database(suite: SuiteGuard<DatabaseSuite>) {
83//!     // suite.connection_string is available
84//!     // Drop is called automatically when last test finishes
85//! }
86//! ```
87
88use async_dropper::{AsyncDrop, AsyncDropper};
89use std::ops::{Deref, DerefMut};
90use std::sync::{Arc, Mutex, OnceLock, Weak};
91
92/// Per-test resource with setup and teardown hooks
93///
94/// Implement this trait to define test resources that need
95/// initialization before each test and cleanup after each test.
96///
97/// # Examples
98///
99/// ```rust
100/// use reinhardt_testkit::resource::TestResource;
101///
102/// struct TestEnv {
103///     data: Vec<String>,
104/// }
105///
106/// impl TestResource for TestEnv {
107///     fn setup() -> Self {
108///         Self { data: vec![] }
109///     }
110///
111///     fn teardown(&mut self) {
112///         self.data.clear();
113///     }
114/// }
115/// ```
116pub trait TestResource: Sized {
117	/// Setup hook called before each test (BeforeEach)
118	fn setup() -> Self;
119
120	/// Teardown hook called after each test (AfterEach)
121	///
122	/// This is called automatically by `TeardownGuard::drop`,
123	/// ensuring cleanup even if the test panics.
124	fn teardown(&mut self);
125}
126
127/// RAII guard for automatic test resource cleanup
128///
129/// This guard ensures `teardown()` is called when the guard
130/// goes out of scope, even if the test panics.
131///
132/// # Examples
133///
134/// ```rust
135/// use reinhardt_testkit::resource::{TestResource, TeardownGuard};
136/// use rstest::*;
137///
138/// struct TestEnv;
139///
140/// impl TestResource for TestEnv {
141///     fn setup() -> Self { Self }
142///     fn teardown(&mut self) { }
143/// }
144///
145/// #[fixture]
146/// fn ctx() -> TeardownGuard<TestEnv> {
147///     TeardownGuard::new()
148/// }
149///
150/// #[rstest]
151/// fn test_example(ctx: TeardownGuard<TestEnv>) {
152///     // Test code here
153///     // teardown() is automatically called
154/// }
155/// ```
156pub struct TeardownGuard<F: TestResource>(F);
157
158impl<F: TestResource> TeardownGuard<F> {
159	/// Create a new teardown guard with resource setup
160	pub fn new() -> Self {
161		Self(F::setup())
162	}
163}
164
165impl<F: TestResource> Default for TeardownGuard<F> {
166	fn default() -> Self {
167		Self::new()
168	}
169}
170
171impl<F: TestResource> Drop for TeardownGuard<F> {
172	fn drop(&mut self) {
173		self.0.teardown();
174	}
175}
176
177impl<F: TestResource> Deref for TeardownGuard<F> {
178	type Target = F;
179
180	fn deref(&self) -> &F {
181		&self.0
182	}
183}
184
185impl<F: TestResource> DerefMut for TeardownGuard<F> {
186	fn deref_mut(&mut self) -> &mut F {
187		&mut self.0
188	}
189}
190
191/// Suite-wide shared resource (BeforeAll/AfterAll pattern)
192///
193/// Implement this trait for resources that should be shared
194/// across multiple tests and cleaned up when all tests complete.
195///
196/// # Examples
197///
198/// ```rust,no_run
199/// use reinhardt_testkit::resource::SuiteResource;
200///
201/// struct DatabaseSuite {
202///     url: String,
203/// }
204///
205/// impl SuiteResource for DatabaseSuite {
206///     fn init() -> Self {
207///         // Expensive setup
208///         Self { url: "postgres://localhost/test".to_string() }
209///     }
210/// }
211///
212/// impl Drop for DatabaseSuite {
213///     fn drop(&mut self) {
214///         // Cleanup when last test finishes
215///         println!("Shutting down test database");
216///     }
217/// }
218/// ```
219pub trait SuiteResource: Send + Sync + 'static {
220	/// Initialize suite resource (BeforeAll)
221	///
222	/// This is called once when the first test needs the resource.
223	fn init() -> Self;
224}
225
226/// Guard for suite-wide shared resource
227///
228/// Uses `OnceLock + Weak<Arc<T>>` pattern to ensure:
229/// - Resource is initialized only once
230/// - Resource is dropped when last test completes
231///
232/// # Examples
233///
234/// ```rust,no_run
235/// use reinhardt_testkit::resource::{SuiteResource, SuiteGuard, acquire_suite};
236/// use rstest::*;
237/// use std::sync::{OnceLock, Mutex, Weak};
238///
239/// struct MySuite;
240///
241/// impl SuiteResource for MySuite {
242///     fn init() -> Self { Self }
243/// }
244///
245/// static SUITE: OnceLock<Mutex<Weak<MySuite>>> = OnceLock::new();
246///
247/// #[fixture]
248/// fn suite() -> SuiteGuard<MySuite> {
249///     acquire_suite(&SUITE)
250/// }
251/// ```
252pub struct SuiteGuard<T: SuiteResource> {
253	arc: Arc<T>,
254}
255
256impl<T: SuiteResource> Deref for SuiteGuard<T> {
257	type Target = T;
258
259	fn deref(&self) -> &T {
260		&self.arc
261	}
262}
263
264/// Acquire suite-wide shared resource
265///
266/// This function uses `OnceLock + Weak<Arc<T>>` pattern to:
267/// 1. Initialize resource once on first call
268/// 2. Reuse existing resource for subsequent calls
269/// 3. Drop resource when last guard is dropped
270///
271/// # Examples
272///
273/// ```rust,no_run
274/// use reinhardt_testkit::resource::{SuiteResource, acquire_suite};
275/// use std::sync::{OnceLock, Mutex, Weak};
276///
277/// struct MySuite {
278///     value: i32,
279/// }
280///
281/// impl SuiteResource for MySuite {
282///     fn init() -> Self {
283///         Self { value: 42 }
284///     }
285/// }
286///
287/// static SUITE: OnceLock<Mutex<Weak<MySuite>>> = OnceLock::new();
288///
289/// let guard1 = acquire_suite(&SUITE);
290/// let guard2 = acquire_suite(&SUITE);  // Reuses same instance
291/// assert_eq!(guard1.value, 42);
292/// assert_eq!(guard2.value, 42);
293/// ```
294pub fn acquire_suite<T: SuiteResource>(cell: &'static OnceLock<Mutex<Weak<T>>>) -> SuiteGuard<T> {
295	let mutex = cell.get_or_init(|| Mutex::new(Weak::new()));
296
297	// Recover from poisoned mutex to prevent test suite cascade failures.
298	// A poisoned mutex means a previous test panicked while holding the lock,
299	// but the Weak<T> inside is still safe to read and upgrade.
300	let mut weak = mutex.lock().unwrap_or_else(|poisoned| {
301		eprintln!(
302			"[suite-resource] Recovering from poisoned mutex \
303			 (a previous test panicked while holding the lock)"
304		);
305		poisoned.into_inner()
306	});
307
308	// Try to upgrade existing Weak reference
309	if let Some(existing) = weak.upgrade() {
310		return SuiteGuard { arc: existing };
311	}
312
313	// Initialize new resource
314	let arc = Arc::new(T::init());
315	*weak = Arc::downgrade(&arc);
316
317	SuiteGuard { arc }
318}
319
320/// Async version of TestResource for async setup/teardown
321///
322/// Implement this trait for test resources that require
323/// asynchronous initialization or cleanup.
324///
325/// # Examples
326///
327/// ```rust
328/// use reinhardt_testkit::resource::AsyncTestResource;
329///
330/// struct AsyncTestEnv {
331///     connection: String,
332/// }
333///
334/// #[async_trait::async_trait]
335/// impl AsyncTestResource for AsyncTestEnv {
336///     async fn setup() -> Self {
337///         // Async initialization
338///         Self { connection: "test_db".to_string() }
339///     }
340///
341///     async fn teardown(self) {
342///         // Async cleanup
343///     }
344/// }
345/// ```
346#[async_trait::async_trait]
347pub trait AsyncTestResource: Sized + Send {
348	/// Async setup hook called before each test
349	async fn setup() -> Self;
350
351	/// Async teardown hook called after each test
352	///
353	/// Takes ownership of self to ensure cleanup.
354	async fn teardown(self);
355}
356
357// Internal wrapper for async drop implementation
358struct AsyncResourceWrapper<F: AsyncTestResource> {
359	resource: Option<F>,
360}
361
362impl<F: AsyncTestResource> Default for AsyncResourceWrapper<F> {
363	fn default() -> Self {
364		Self { resource: None }
365	}
366}
367
368#[async_trait::async_trait]
369impl<F: AsyncTestResource> AsyncDrop for AsyncResourceWrapper<F> {
370	async fn async_drop(&mut self) {
371		if let Some(resource) = self.resource.take() {
372			resource.teardown().await;
373		}
374	}
375}
376
377/// RAII guard for async test resource cleanup using async-dropper
378///
379/// This guard automatically calls `teardown()` when dropped, using the `async-dropper` crate
380/// to properly handle async cleanup in Drop.
381///
382/// # Important
383///
384/// **Requires multi-threaded tokio runtime.** Use `#[tokio::test(flavor = "multi_thread")]`
385/// instead of `#[tokio::test]` because async-dropper uses blocking operations internally.
386///
387/// # Examples
388///
389/// ```rust
390/// use reinhardt_testkit::resource::{AsyncTestResource, AsyncTeardownGuard};
391///
392/// struct AsyncTestEnv {
393///     value: i32,
394/// }
395///
396/// #[async_trait::async_trait]
397/// impl AsyncTestResource for AsyncTestEnv {
398///     async fn setup() -> Self {
399///         Self { value: 42 }
400///     }
401///     async fn teardown(self) {
402///         // Cleanup code here
403///     }
404/// }
405///
406/// #[tokio::test(flavor = "multi_thread")]
407/// async fn test_example() {
408///     let guard = AsyncTeardownGuard::<AsyncTestEnv>::new().await;
409///     assert_eq!(guard.value, 42);
410///     // Cleanup automatically called when guard is dropped
411/// }
412/// ```
413pub struct AsyncTeardownGuard<F: AsyncTestResource + 'static> {
414	inner: AsyncDropper<AsyncResourceWrapper<F>>,
415}
416
417impl<F: AsyncTestResource + 'static> AsyncTeardownGuard<F> {
418	/// Create a new async teardown guard with resource setup
419	pub async fn new() -> Self {
420		let resource = F::setup().await;
421		let wrapper = AsyncResourceWrapper {
422			resource: Some(resource),
423		};
424		Self {
425			inner: AsyncDropper::new(wrapper),
426		}
427	}
428}
429
430impl<F: AsyncTestResource + 'static> Deref for AsyncTeardownGuard<F> {
431	type Target = F;
432
433	fn deref(&self) -> &F {
434		// Access resource through AsyncDropper -> AsyncResourceWrapper -> F
435		self.inner
436			.inner()
437			.resource
438			.as_ref()
439			.expect("Resource already dropped")
440	}
441}
442
443impl<F: AsyncTestResource + 'static> DerefMut for AsyncTeardownGuard<F> {
444	fn deref_mut(&mut self) -> &mut F {
445		// Access resource through AsyncDropper -> AsyncResourceWrapper -> F
446		self.inner
447			.inner_mut()
448			.resource
449			.as_mut()
450			.expect("Resource already dropped")
451	}
452}
453
454#[cfg(test)]
455mod tests {
456	use super::*;
457
458	struct Counter {
459		setup_count: usize,
460		teardown_count: usize,
461	}
462
463	impl TestResource for Counter {
464		fn setup() -> Self {
465			Self {
466				setup_count: 1,
467				teardown_count: 0,
468			}
469		}
470
471		fn teardown(&mut self) {
472			self.teardown_count += 1;
473		}
474	}
475
476	#[test]
477	fn test_teardown_guard() {
478		let mut guard = TeardownGuard::<Counter>::new();
479		assert_eq!(guard.setup_count, 1);
480		assert_eq!(guard.teardown_count, 0);
481
482		// Manually trigger teardown for testing
483		guard.teardown();
484		assert_eq!(guard.teardown_count, 1);
485	}
486
487	struct SuiteCounter {
488		value: i32,
489	}
490
491	impl SuiteResource for SuiteCounter {
492		fn init() -> Self {
493			Self { value: 42 }
494		}
495	}
496
497	#[test]
498	fn test_suite_guard() {
499		static SUITE: OnceLock<Mutex<Weak<SuiteCounter>>> = OnceLock::new();
500
501		let guard1 = acquire_suite(&SUITE);
502		assert_eq!(guard1.value, 42);
503
504		let guard2 = acquire_suite(&SUITE);
505		assert_eq!(guard2.value, 42);
506
507		// Both guards should point to the same instance
508		assert!(Arc::ptr_eq(&guard1.arc, &guard2.arc));
509	}
510
511	struct AsyncCounter {
512		value: i32,
513	}
514
515	#[async_trait::async_trait]
516	impl AsyncTestResource for AsyncCounter {
517		async fn setup() -> Self {
518			Self { value: 42 }
519		}
520
521		async fn teardown(self) {
522			// Cleanup
523		}
524	}
525
526	#[tokio::test(flavor = "multi_thread")]
527	async fn test_async_teardown_guard_auto_cleanup() {
528		// Test automatic cleanup when guard is dropped in tokio runtime
529		{
530			let guard = AsyncTeardownGuard::<AsyncCounter>::new().await;
531			assert_eq!(guard.value, 42);
532			// Guard is automatically cleaned up when dropped
533		}
534		// async-dropper ensures cleanup completes before continuing
535	}
536}