use parking_lot::RwLock;
use std::{
future::Future,
sync::Arc,
time::{Duration, Instant},
};
use wae_types::{WaeError, WaeErrorKind, WaeResult as TestingResult};
mod builder;
mod config;
mod hooks;
mod state;
pub use builder::TestEnvBuilder;
pub use config::{TestEnvConfig, TestServiceConfig};
pub use hooks::{AsyncTestLifecycleHook, TestLifecycleHook};
pub use state::TestEnvState;
pub struct TestEnv {
config: TestEnvConfig,
state: Arc<RwLock<TestEnvState>>,
created_at: Instant,
initialized_at: Arc<RwLock<Option<Instant>>>,
lifecycle_hooks: Arc<RwLock<Vec<Box<dyn TestLifecycleHook>>>>,
async_lifecycle_hooks: Arc<RwLock<Vec<Box<dyn AsyncTestLifecycleHook>>>>,
#[allow(clippy::type_complexity)]
cleanup_handlers: Arc<RwLock<Vec<Box<dyn Fn() + Send + Sync>>>>,
#[allow(clippy::type_complexity)]
async_cleanup_handlers: Arc<RwLock<Vec<Box<dyn Fn() -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>>>>,
storage: Arc<RwLock<std::collections::HashMap<String, Box<dyn std::any::Any + Send + Sync>>>>,
services: Arc<RwLock<std::collections::HashMap<String, TestServiceConfig>>>,
}
impl std::fmt::Debug for TestEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestEnv")
.field("config", &self.config)
.field("state", &self.state)
.field("created_at", &self.created_at)
.field("initialized_at", &self.initialized_at)
.field("services", &self.services)
.finish()
}
}
impl TestEnv {
pub fn new(config: TestEnvConfig) -> Self {
Self {
config,
state: Arc::new(RwLock::new(TestEnvState::Uninitialized)),
created_at: Instant::now(),
initialized_at: Arc::new(RwLock::new(None)),
lifecycle_hooks: Arc::new(RwLock::new(Vec::new())),
async_lifecycle_hooks: Arc::new(RwLock::new(Vec::new())),
cleanup_handlers: Arc::new(RwLock::new(Vec::new())),
async_cleanup_handlers: Arc::new(RwLock::new(Vec::new())),
storage: Arc::new(RwLock::new(std::collections::HashMap::new())),
services: Arc::new(RwLock::new(std::collections::HashMap::new())),
}
}
pub fn default_env() -> Self {
Self::new(TestEnvConfig::default())
}
pub fn setup(&self) -> TestingResult<()> {
{
let mut state = self.state.write();
if *state != TestEnvState::Uninitialized {
return Err(WaeError::new(WaeErrorKind::EnvironmentError {
reason: "Environment already initialized".to_string(),
}));
}
*state = TestEnvState::Initializing;
}
let result = (|| {
for hook in self.lifecycle_hooks.read().iter() {
hook.before_setup(self)?;
}
for hook in self.lifecycle_hooks.read().iter() {
hook.after_setup(self)?;
}
Ok(())
})();
let mut state = self.state.write();
match result {
Ok(_) => {
*state = TestEnvState::Initialized;
*self.initialized_at.write() = Some(Instant::now());
Ok(())
}
Err(e) => {
*state = TestEnvState::Uninitialized;
Err(e)
}
}
}
pub async fn setup_async(&self) -> TestingResult<()> {
{
let mut state = self.state.write();
if *state != TestEnvState::Uninitialized {
return Err(WaeError::new(WaeErrorKind::EnvironmentError {
reason: "Environment already initialized".to_string(),
}));
}
*state = TestEnvState::Initializing;
}
let result = (async {
for hook in self.lifecycle_hooks.read().iter() {
hook.before_setup(self)?;
}
#[allow(clippy::await_holding_lock)]
for hook in self.async_lifecycle_hooks.read().iter() {
hook.before_setup_async(self).await?;
}
#[allow(clippy::await_holding_lock)]
for hook in self.async_lifecycle_hooks.read().iter() {
hook.after_setup_async(self).await?;
}
for hook in self.lifecycle_hooks.read().iter() {
hook.after_setup(self)?;
}
Ok(())
})
.await;
let mut state = self.state.write();
match result {
Ok(_) => {
*state = TestEnvState::Initialized;
*self.initialized_at.write() = Some(Instant::now());
Ok(())
}
Err(e) => {
*state = TestEnvState::Uninitialized;
Err(e)
}
}
}
pub fn teardown(&self) -> TestingResult<()> {
{
let mut state = self.state.write();
if *state != TestEnvState::Initialized {
return Err(WaeError::new(WaeErrorKind::EnvironmentError {
reason: "Environment not initialized".to_string(),
}));
}
*state = TestEnvState::Destroying;
}
let result = (|| {
for hook in self.lifecycle_hooks.read().iter() {
hook.before_teardown(self)?;
}
let handlers = self.cleanup_handlers.write();
for handler in handlers.iter().rev() {
handler();
}
self.storage.write().clear();
for hook in self.lifecycle_hooks.read().iter() {
hook.after_teardown(self)?;
}
Ok(())
})();
let mut state = self.state.write();
*state = TestEnvState::Destroyed;
result
}
pub async fn teardown_async(&self) -> TestingResult<()> {
{
let mut state = self.state.write();
if *state != TestEnvState::Initialized {
return Err(WaeError::new(WaeErrorKind::EnvironmentError {
reason: "Environment not initialized".to_string(),
}));
}
*state = TestEnvState::Destroying;
}
let result = (async {
for hook in self.lifecycle_hooks.read().iter() {
hook.before_teardown(self)?;
}
#[allow(clippy::await_holding_lock)]
for hook in self.async_lifecycle_hooks.read().iter() {
hook.before_teardown_async(self).await?;
}
#[allow(clippy::await_holding_lock)]
{
let handlers = self.async_cleanup_handlers.write();
for handler in handlers.iter().rev() {
handler().await;
}
}
{
let handlers = self.cleanup_handlers.write();
for handler in handlers.iter().rev() {
handler();
}
}
self.storage.write().clear();
#[allow(clippy::await_holding_lock)]
for hook in self.async_lifecycle_hooks.read().iter() {
hook.after_teardown_async(self).await?;
}
for hook in self.lifecycle_hooks.read().iter() {
hook.after_teardown(self)?;
}
Ok(())
})
.await;
let mut state = self.state.write();
*state = TestEnvState::Destroyed;
result
}
pub fn state(&self) -> TestEnvState {
self.state.read().clone()
}
pub fn elapsed(&self) -> Duration {
self.created_at.elapsed()
}
pub fn initialized_elapsed(&self) -> Option<Duration> {
self.initialized_at.read().map(|t| t.elapsed())
}
pub fn add_lifecycle_hook<H>(&self, hook: H)
where
H: TestLifecycleHook + 'static,
{
self.lifecycle_hooks.write().push(Box::new(hook));
}
pub fn add_async_lifecycle_hook<H>(&self, hook: H)
where
H: AsyncTestLifecycleHook + 'static,
{
self.async_lifecycle_hooks.write().push(Box::new(hook));
}
pub fn on_cleanup<F>(&self, handler: F)
where
F: Fn() + Send + Sync + 'static,
{
self.cleanup_handlers.write().push(Box::new(handler));
}
pub fn on_cleanup_async<F, Fut>(&self, handler: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.async_cleanup_handlers.write().push(Box::new(move || Box::pin(handler())));
}
pub fn set<T: 'static + Send + Sync>(&self, key: &str, value: T) {
self.storage.write().insert(key.to_string(), Box::new(value));
}
pub fn get<T: 'static + Clone>(&self, key: &str) -> Option<T> {
let storage = self.storage.read();
storage.get(key).and_then(|v| v.downcast_ref::<T>().cloned())
}
pub fn remove<T: 'static>(&self, key: &str) -> Option<T> {
let mut storage = self.storage.write();
storage.remove(key).and_then(|v| v.downcast::<T>().ok()).map(|v| *v)
}
pub fn has(&self, key: &str) -> bool {
self.storage.read().contains_key(key)
}
pub fn config(&self) -> &TestEnvConfig {
&self.config
}
pub fn add_service(&self, service_config: TestServiceConfig) {
self.services.write().insert(service_config.name.clone(), service_config);
}
pub fn get_service(&self, name: &str) -> Option<TestServiceConfig> {
self.services.read().get(name).cloned()
}
pub fn enabled_services(&self) -> Vec<TestServiceConfig> {
self.services.read().values().filter(|s| s.enabled).cloned().collect()
}
pub async fn with_fixture<F, R>(&self, fixture: F) -> TestingResult<R>
where
F: FnOnce() -> TestingResult<R>,
{
self.setup()?;
let result = fixture();
self.teardown()?;
result
}
pub async fn run_test<F, Fut>(&self, test: F) -> TestingResult<()>
where
F: FnOnce() -> Fut,
Fut: Future<Output = TestingResult<()>>,
{
self.setup()?;
let result = test().await;
self.teardown()?;
result
}
pub async fn run_test_async<F, Fut>(&self, test: F) -> TestingResult<()>
where
F: FnOnce() -> Fut,
Fut: Future<Output = TestingResult<()>>,
{
self.setup_async().await?;
let result = test().await;
self.teardown_async().await?;
result
}
}
impl Drop for TestEnv {
fn drop(&mut self) {
let state = self.state.read().clone();
if state == TestEnvState::Initialized {
let _ = self.teardown();
}
}
}
pub fn create_test_env() -> TestEnv {
TestEnv::default_env()
}
pub fn create_test_env_with_config(config: TestEnvConfig) -> TestEnv {
TestEnv::new(config)
}