Skip to main content

windjammer_runtime/
fixtures.rs

1//! Fixture system for tests
2//!
3//! Provides a registry for test fixtures with automatic lifecycle management.
4
5use std::any::{Any, TypeId};
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9type FixtureFactory = Box<dyn Fn() -> Box<dyn Any + Send> + Send + Sync>;
10
11/// Global fixture registry
12static FIXTURE_REGISTRY: Mutex<Option<FixtureRegistry>> = Mutex::new(None);
13
14/// Fixture registry for managing test resources
15pub struct FixtureRegistry {
16    factories: HashMap<String, FixtureFactory>,
17    type_ids: HashMap<String, TypeId>,
18}
19
20impl FixtureRegistry {
21    pub fn new() -> Self {
22        Self {
23            factories: HashMap::new(),
24            type_ids: HashMap::new(),
25        }
26    }
27
28    /// Register a fixture with a name and factory function
29    pub fn register<T: 'static + Send>(
30        &mut self,
31        name: &str,
32        factory: impl Fn() -> T + 'static + Send + Sync,
33    ) {
34        self.factories
35            .insert(name.to_string(), Box::new(move || Box::new(factory())));
36        self.type_ids.insert(name.to_string(), TypeId::of::<T>());
37    }
38
39    /// Get a fixture by name
40    pub fn get<T: 'static>(&self, name: &str) -> Option<T> {
41        let type_id = self.type_ids.get(name)?;
42        if *type_id != TypeId::of::<T>() {
43            return None;
44        }
45
46        let factory = self.factories.get(name)?;
47        let boxed = factory();
48        boxed.downcast::<T>().ok().map(|b| *b)
49    }
50}
51
52impl Default for FixtureRegistry {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58/// Get the global fixture registry
59pub fn global_registry() -> Arc<Mutex<FixtureRegistry>> {
60    let mut guard = FIXTURE_REGISTRY.lock().unwrap();
61    if guard.is_none() {
62        *guard = Some(FixtureRegistry::new());
63    }
64    drop(guard);
65
66    // Return a reference to the registry
67    Arc::new(Mutex::new(FixtureRegistry::new()))
68}
69
70/// Register a fixture globally
71pub fn register_fixture<T: 'static + Send>(
72    name: &str,
73    factory: impl Fn() -> T + 'static + Send + Sync,
74) {
75    let registry = global_registry();
76    let mut guard = registry.lock().unwrap();
77    guard.register(name, factory);
78}
79
80/// Use a fixture by name
81pub fn use_fixture<T: 'static>(name: &str) -> Option<T> {
82    let registry = global_registry();
83    let guard = registry.lock().unwrap();
84    guard.get(name)
85}
86
87/// Fixture scope helper - automatically drops fixture at end of scope
88pub struct FixtureScope<T> {
89    value: Option<T>,
90}
91
92impl<T> FixtureScope<T> {
93    pub fn new(value: T) -> Self {
94        Self { value: Some(value) }
95    }
96
97    pub fn get(&self) -> &T {
98        self.value.as_ref().unwrap()
99    }
100
101    pub fn get_mut(&mut self) -> &mut T {
102        self.value.as_mut().unwrap()
103    }
104}
105
106impl<T> Drop for FixtureScope<T> {
107    fn drop(&mut self) {
108        // Cleanup happens automatically via Drop
109        self.value.take();
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[derive(Debug, Clone, PartialEq)]
118    struct TestData {
119        value: i32,
120    }
121
122    #[test]
123    fn test_fixture_registry() {
124        let mut registry = FixtureRegistry::new();
125
126        registry.register("test_data", || TestData { value: 42 });
127
128        let data: Option<TestData> = registry.get("test_data");
129        assert!(data.is_some());
130        assert_eq!(data.unwrap().value, 42);
131    }
132
133    #[test]
134    fn test_fixture_wrong_type() {
135        let mut registry = FixtureRegistry::new();
136
137        registry.register("test_data", || TestData { value: 42 });
138
139        // Try to get with wrong type
140        let data: Option<String> = registry.get("test_data");
141        assert!(data.is_none());
142    }
143
144    #[test]
145    fn test_fixture_not_found() {
146        let registry = FixtureRegistry::new();
147
148        let data: Option<TestData> = registry.get("nonexistent");
149        assert!(data.is_none());
150    }
151
152    #[test]
153    fn test_fixture_scope() {
154        let scope = FixtureScope::new(TestData { value: 42 });
155        assert_eq!(scope.get().value, 42);
156
157        // scope is dropped here, cleanup happens automatically
158    }
159
160    #[test]
161    fn test_fixture_scope_mut() {
162        let mut scope = FixtureScope::new(TestData { value: 42 });
163        scope.get_mut().value = 100;
164        assert_eq!(scope.get().value, 100);
165    }
166}