fluent_test/backend/fixtures/
mod.rs

1//! Module for test fixtures support with setup and teardown capabilities
2//!
3//! This module provides the runtime functionality for test fixtures using attributes.
4//! It works with procedural macros to provide a clean API for setting up and tearing
5//! down test environments.
6
7use once_cell::sync::Lazy;
8use std::cell::RefCell;
9use std::collections::{HashMap, HashSet};
10use std::panic::{self, AssertUnwindSafe};
11use std::sync::Mutex;
12
13/// Simple fixture registration system that uses a global hashmap instead of inventory
14pub type FixtureFunc = Box<dyn Fn() + Send + Sync + 'static>;
15
16static SETUP_FIXTURES: Lazy<Mutex<HashMap<&'static str, Vec<FixtureFunc>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
17
18static TEARDOWN_FIXTURES: Lazy<Mutex<HashMap<&'static str, Vec<FixtureFunc>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
19
20static BEFORE_ALL_FIXTURES: Lazy<Mutex<HashMap<&'static str, Vec<FixtureFunc>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
21
22static AFTER_ALL_FIXTURES: Lazy<Mutex<HashMap<&'static str, Vec<FixtureFunc>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
23
24static EXECUTED_MODULES: Lazy<Mutex<HashSet<&'static str>>> = Lazy::new(|| Mutex::new(HashSet::new()));
25
26/// Register a setup function for a module
27///
28/// This is automatically called by the `#[setup]` attribute macro.
29pub fn register_setup(module_path: &'static str, func: FixtureFunc) {
30    let mut fixtures = SETUP_FIXTURES.lock().unwrap();
31    fixtures.entry(module_path).or_default().push(func);
32}
33
34/// Register a teardown function for a module
35///
36/// This is automatically called by the `#[tear_down]` attribute macro.
37pub fn register_teardown(module_path: &'static str, func: FixtureFunc) {
38    let mut fixtures = TEARDOWN_FIXTURES.lock().unwrap();
39    fixtures.entry(module_path).or_default().push(func);
40}
41
42/// Register a before_all function for a module
43///
44/// This is automatically called by the `#[before_all]` attribute macro.
45/// These functions run once before any test in the module.
46pub fn register_before_all(module_path: &'static str, func: FixtureFunc) {
47    let mut fixtures = BEFORE_ALL_FIXTURES.lock().unwrap();
48    fixtures.entry(module_path).or_default().push(func);
49}
50
51/// Register an after_all function for a module
52///
53/// This is automatically called by the `#[after_all]` attribute macro.
54/// These functions run once after all tests in the module.
55/// Note: In standalone test execution, this is guaranteed to run.
56/// But in parallel test execution, it depends on the test runner.
57pub fn register_after_all(module_path: &'static str, func: FixtureFunc) {
58    let mut fixtures = AFTER_ALL_FIXTURES.lock().unwrap();
59    fixtures.entry(module_path).or_default().push(func);
60}
61
62thread_local! {
63    /// Indicator of whether we're currently in a fixture-wrapped test
64    static IN_FIXTURE_TEST: RefCell<bool> = const { RefCell::new(false) };
65}
66
67/// Run a test function with appropriate setup and teardown
68///
69/// This is automatically called by the `#[with_fixtures]` attribute macro.
70pub fn run_test_with_fixtures<F>(module_path: &'static str, test_fn: AssertUnwindSafe<F>)
71where
72    F: FnOnce(),
73{
74    // Set the fixture test flag
75    IN_FIXTURE_TEST.with(|flag| {
76        *flag.borrow_mut() = true;
77    });
78
79    // Check if before_all fixtures have been run for this module
80    // and run them if they haven't
81    run_before_all_if_needed(module_path);
82
83    // Run setup functions for this module if any exist
84    if let Ok(fixtures) = SETUP_FIXTURES.lock() {
85        if let Some(setup_funcs) = fixtures.get(module_path) {
86            for setup_fn in setup_funcs {
87                setup_fn();
88            }
89        }
90    }
91
92    // Run the test function, capturing any panics
93    let result = panic::catch_unwind(test_fn);
94
95    // Always run teardown, even if the test panics
96    if let Ok(fixtures) = TEARDOWN_FIXTURES.lock() {
97        if let Some(teardown_funcs) = fixtures.get(module_path) {
98            for teardown_fn in teardown_funcs {
99                teardown_fn();
100            }
101        }
102    }
103
104    // Reset the fixture test flag
105    IN_FIXTURE_TEST.with(|flag| {
106        *flag.borrow_mut() = false;
107    });
108
109    // Register after_all fixtures to be run at process exit
110    // We can't run them now because we don't know if this is the last test
111    register_after_all_handler(module_path);
112
113    // Re-throw any panic that occurred during the test
114    if let Err(err) = result {
115        panic::resume_unwind(err);
116    }
117}
118
119/// Run before_all fixtures for a module if they haven't been run yet
120fn run_before_all_if_needed(module_path: &'static str) {
121    // Check if we've already executed the before_all fixtures for this module
122    let mut executed = EXECUTED_MODULES.lock().unwrap();
123    if !executed.contains(module_path) {
124        // Mark as executed first to prevent potential infinite recursion
125        executed.insert(module_path);
126
127        // Run before_all fixtures
128        if let Ok(fixtures) = BEFORE_ALL_FIXTURES.lock() {
129            if let Some(before_all_funcs) = fixtures.get(module_path) {
130                for before_fn in before_all_funcs {
131                    before_fn();
132                }
133            }
134        }
135    }
136}
137
138/// Register after_all fixtures to be run at process exit
139fn register_after_all_handler(module_path: &'static str) {
140    // We use ctor's dtor to register a function that will run at process exit
141    // This is a bit of a hack, but it's the best we can do without modifying the test runner
142    // The actual registration happens in the macro
143
144    // Here we just ensure the module path is saved for the handler
145    let mut executed = EXECUTED_MODULES.lock().unwrap();
146    executed.insert(module_path);
147}
148
149/// Run all after_all fixtures that have been registered
150/// This is called by an exit handler registered by the test runner
151#[doc(hidden)]
152pub fn run_after_all_fixtures() {
153    // Get the list of modules that have been executed
154    let executed = EXECUTED_MODULES.lock().unwrap();
155
156    // Run after_all fixtures for each executed module
157    if let Ok(fixtures) = AFTER_ALL_FIXTURES.lock() {
158        for module_path in executed.iter() {
159            if let Some(after_all_funcs) = fixtures.get(module_path) {
160                for after_fn in after_all_funcs {
161                    after_fn();
162                }
163            }
164        }
165    }
166}
167
168/// Check if we're running inside a fixture-wrapped test
169pub fn is_in_fixture_test() -> bool {
170    return IN_FIXTURE_TEST.with(|flag| *flag.borrow());
171}