wae_testing/environment/
mod.rs1use parking_lot::RwLock;
6use std::{
7 future::Future,
8 sync::Arc,
9 time::{Duration, Instant},
10};
11use wae_types::{WaeError, WaeErrorKind, WaeResult as TestingResult};
12
13mod builder;
14mod config;
15mod hooks;
16mod state;
17
18pub use builder::TestEnvBuilder;
19pub use config::{TestEnvConfig, TestServiceConfig};
20pub use hooks::{AsyncTestLifecycleHook, TestLifecycleHook};
21pub use state::TestEnvState;
22
23pub struct TestEnv {
27 config: TestEnvConfig,
29 state: Arc<RwLock<TestEnvState>>,
31 created_at: Instant,
33 initialized_at: Arc<RwLock<Option<Instant>>>,
35 lifecycle_hooks: Arc<RwLock<Vec<Box<dyn TestLifecycleHook>>>>,
37 async_lifecycle_hooks: Arc<RwLock<Vec<Box<dyn AsyncTestLifecycleHook>>>>,
39 #[allow(clippy::type_complexity)]
41 cleanup_handlers: Arc<RwLock<Vec<Box<dyn Fn() + Send + Sync>>>>,
42 #[allow(clippy::type_complexity)]
44 async_cleanup_handlers: Arc<RwLock<Vec<Box<dyn Fn() -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>>>>,
45 storage: Arc<RwLock<std::collections::HashMap<String, Box<dyn std::any::Any + Send + Sync>>>>,
47 services: Arc<RwLock<std::collections::HashMap<String, TestServiceConfig>>>,
49}
50
51impl std::fmt::Debug for TestEnv {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("TestEnv")
54 .field("config", &self.config)
55 .field("state", &self.state)
56 .field("created_at", &self.created_at)
57 .field("initialized_at", &self.initialized_at)
58 .field("services", &self.services)
59 .finish()
60 }
61}
62
63impl TestEnv {
64 pub fn new(config: TestEnvConfig) -> Self {
75 Self {
76 config,
77 state: Arc::new(RwLock::new(TestEnvState::Uninitialized)),
78 created_at: Instant::now(),
79 initialized_at: Arc::new(RwLock::new(None)),
80 lifecycle_hooks: Arc::new(RwLock::new(Vec::new())),
81 async_lifecycle_hooks: Arc::new(RwLock::new(Vec::new())),
82 cleanup_handlers: Arc::new(RwLock::new(Vec::new())),
83 async_cleanup_handlers: Arc::new(RwLock::new(Vec::new())),
84 storage: Arc::new(RwLock::new(std::collections::HashMap::new())),
85 services: Arc::new(RwLock::new(std::collections::HashMap::new())),
86 }
87 }
88
89 pub fn default_env() -> Self {
99 Self::new(TestEnvConfig::default())
100 }
101
102 pub fn setup(&self) -> TestingResult<()> {
119 {
120 let mut state = self.state.write();
121 if *state != TestEnvState::Uninitialized {
122 return Err(WaeError::new(WaeErrorKind::EnvironmentError {
123 reason: "Environment already initialized".to_string(),
124 }));
125 }
126 *state = TestEnvState::Initializing;
127 }
128
129 let result = (|| {
130 for hook in self.lifecycle_hooks.read().iter() {
131 hook.before_setup(self)?;
132 }
133
134 for hook in self.lifecycle_hooks.read().iter() {
135 hook.after_setup(self)?;
136 }
137
138 Ok(())
139 })();
140
141 let mut state = self.state.write();
142 match result {
143 Ok(_) => {
144 *state = TestEnvState::Initialized;
145 *self.initialized_at.write() = Some(Instant::now());
146 Ok(())
147 }
148 Err(e) => {
149 *state = TestEnvState::Uninitialized;
150 Err(e)
151 }
152 }
153 }
154
155 pub async fn setup_async(&self) -> TestingResult<()> {
163 {
164 let mut state = self.state.write();
165 if *state != TestEnvState::Uninitialized {
166 return Err(WaeError::new(WaeErrorKind::EnvironmentError {
167 reason: "Environment already initialized".to_string(),
168 }));
169 }
170 *state = TestEnvState::Initializing;
171 }
172
173 let result = (async {
174 for hook in self.lifecycle_hooks.read().iter() {
175 hook.before_setup(self)?;
176 }
177
178 #[allow(clippy::await_holding_lock)]
179 for hook in self.async_lifecycle_hooks.read().iter() {
180 hook.before_setup_async(self).await?;
181 }
182
183 #[allow(clippy::await_holding_lock)]
184 for hook in self.async_lifecycle_hooks.read().iter() {
185 hook.after_setup_async(self).await?;
186 }
187
188 for hook in self.lifecycle_hooks.read().iter() {
189 hook.after_setup(self)?;
190 }
191
192 Ok(())
193 })
194 .await;
195
196 let mut state = self.state.write();
197 match result {
198 Ok(_) => {
199 *state = TestEnvState::Initialized;
200 *self.initialized_at.write() = Some(Instant::now());
201 Ok(())
202 }
203 Err(e) => {
204 *state = TestEnvState::Uninitialized;
205 Err(e)
206 }
207 }
208 }
209
210 pub fn teardown(&self) -> TestingResult<()> {
228 {
229 let mut state = self.state.write();
230 if *state != TestEnvState::Initialized {
231 return Err(WaeError::new(WaeErrorKind::EnvironmentError {
232 reason: "Environment not initialized".to_string(),
233 }));
234 }
235 *state = TestEnvState::Destroying;
236 }
237
238 let result = (|| {
239 for hook in self.lifecycle_hooks.read().iter() {
240 hook.before_teardown(self)?;
241 }
242
243 let handlers = self.cleanup_handlers.write();
244 for handler in handlers.iter().rev() {
245 handler();
246 }
247
248 self.storage.write().clear();
249
250 for hook in self.lifecycle_hooks.read().iter() {
251 hook.after_teardown(self)?;
252 }
253
254 Ok(())
255 })();
256
257 let mut state = self.state.write();
258 *state = TestEnvState::Destroyed;
259 result
260 }
261
262 pub async fn teardown_async(&self) -> TestingResult<()> {
270 {
271 let mut state = self.state.write();
272 if *state != TestEnvState::Initialized {
273 return Err(WaeError::new(WaeErrorKind::EnvironmentError {
274 reason: "Environment not initialized".to_string(),
275 }));
276 }
277 *state = TestEnvState::Destroying;
278 }
279
280 let result = (async {
281 for hook in self.lifecycle_hooks.read().iter() {
282 hook.before_teardown(self)?;
283 }
284
285 #[allow(clippy::await_holding_lock)]
286 for hook in self.async_lifecycle_hooks.read().iter() {
287 hook.before_teardown_async(self).await?;
288 }
289
290 #[allow(clippy::await_holding_lock)]
291 {
292 let handlers = self.async_cleanup_handlers.write();
293 for handler in handlers.iter().rev() {
294 handler().await;
295 }
296 }
297
298 {
299 let handlers = self.cleanup_handlers.write();
300 for handler in handlers.iter().rev() {
301 handler();
302 }
303 }
304
305 self.storage.write().clear();
306
307 #[allow(clippy::await_holding_lock)]
308 for hook in self.async_lifecycle_hooks.read().iter() {
309 hook.after_teardown_async(self).await?;
310 }
311
312 for hook in self.lifecycle_hooks.read().iter() {
313 hook.after_teardown(self)?;
314 }
315
316 Ok(())
317 })
318 .await;
319
320 let mut state = self.state.write();
321 *state = TestEnvState::Destroyed;
322 result
323 }
324
325 pub fn state(&self) -> TestEnvState {
336 self.state.read().clone()
337 }
338
339 pub fn elapsed(&self) -> Duration {
343 self.created_at.elapsed()
344 }
345
346 pub fn initialized_elapsed(&self) -> Option<Duration> {
350 self.initialized_at.read().map(|t| t.elapsed())
351 }
352
353 pub fn add_lifecycle_hook<H>(&self, hook: H)
375 where
376 H: TestLifecycleHook + 'static,
377 {
378 self.lifecycle_hooks.write().push(Box::new(hook));
379 }
380
381 pub fn add_async_lifecycle_hook<H>(&self, hook: H)
385 where
386 H: AsyncTestLifecycleHook + 'static,
387 {
388 self.async_lifecycle_hooks.write().push(Box::new(hook));
389 }
390
391 pub fn on_cleanup<F>(&self, handler: F)
404 where
405 F: Fn() + Send + Sync + 'static,
406 {
407 self.cleanup_handlers.write().push(Box::new(handler));
408 }
409
410 pub fn on_cleanup_async<F, Fut>(&self, handler: F)
414 where
415 F: Fn() -> Fut + Send + Sync + 'static,
416 Fut: Future<Output = ()> + Send + 'static,
417 {
418 self.async_cleanup_handlers.write().push(Box::new(move || Box::pin(handler())));
419 }
420
421 pub fn set<T: 'static + Send + Sync>(&self, key: &str, value: T) {
434 self.storage.write().insert(key.to_string(), Box::new(value));
435 }
436
437 pub fn get<T: 'static + Clone>(&self, key: &str) -> Option<T> {
452 let storage = self.storage.read();
453 storage.get(key).and_then(|v| v.downcast_ref::<T>().cloned())
454 }
455
456 pub fn remove<T: 'static>(&self, key: &str) -> Option<T> {
460 let mut storage = self.storage.write();
461 storage.remove(key).and_then(|v| v.downcast::<T>().ok()).map(|v| *v)
462 }
463
464 pub fn has(&self, key: &str) -> bool {
466 self.storage.read().contains_key(key)
467 }
468
469 pub fn config(&self) -> &TestEnvConfig {
481 &self.config
482 }
483
484 pub fn add_service(&self, service_config: TestServiceConfig) {
488 self.services.write().insert(service_config.name.clone(), service_config);
489 }
490
491 pub fn get_service(&self, name: &str) -> Option<TestServiceConfig> {
495 self.services.read().get(name).cloned()
496 }
497
498 pub fn enabled_services(&self) -> Vec<TestServiceConfig> {
502 self.services.read().values().filter(|s| s.enabled).cloned().collect()
503 }
504
505 pub async fn with_fixture<F, R>(&self, fixture: F) -> TestingResult<R>
513 where
514 F: FnOnce() -> TestingResult<R>,
515 {
516 self.setup()?;
517
518 let result = fixture();
519
520 self.teardown()?;
521
522 result
523 }
524
525 pub async fn run_test<F, Fut>(&self, test: F) -> TestingResult<()>
542 where
543 F: FnOnce() -> Fut,
544 Fut: Future<Output = TestingResult<()>>,
545 {
546 self.setup()?;
547
548 let result = test().await;
549
550 self.teardown()?;
551
552 result
553 }
554
555 pub async fn run_test_async<F, Fut>(&self, test: F) -> TestingResult<()>
563 where
564 F: FnOnce() -> Fut,
565 Fut: Future<Output = TestingResult<()>>,
566 {
567 self.setup_async().await?;
568
569 let result = test().await;
570
571 self.teardown_async().await?;
572
573 result
574 }
575}
576
577impl Drop for TestEnv {
578 fn drop(&mut self) {
579 let state = self.state.read().clone();
580 if state == TestEnvState::Initialized {
581 let _ = self.teardown();
582 }
583 }
584}
585
586pub fn create_test_env() -> TestEnv {
598 TestEnv::default_env()
599}
600
601pub fn create_test_env_with_config(config: TestEnvConfig) -> TestEnv {
614 TestEnv::new(config)
615}