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}