use crate::app;
use crate::app::context::AppContext;
use crate::app::prepare::{PrepareOptions, PreparedAppWithoutCli};
use crate::app::{App, prepare, run};
use crate::config::AppConfig;
use crate::error::RoadsterResult;
use crate::service::registry::ServiceRegistry;
use axum_core::extract::FromRef;
use futures::FutureExt;
use std::convert::Infallible;
use std::panic::{AssertUnwindSafe, resume_unwind};
#[non_exhaustive]
pub struct TestAppState<A, S>
where
A: 'static + App<S>,
S: 'static + Send + Sync + Clone,
AppContext: FromRef<S>,
{
pub app: A,
pub state: S,
pub service_registry: ServiceRegistry<S>,
}
pub async fn run_test<A, S>(
app: A,
options: PrepareOptions,
test_fn: impl std::ops::AsyncFnOnce(&TestAppState<A, S>),
) -> RoadsterResult<()>
where
S: 'static + Send + Sync + Clone,
AppContext: FromRef<S>,
A: 'static + Send + Sync + App<S>,
{
let result = run_test_with_result(app, options, async move |app| -> Result<(), Infallible> {
test_fn(app).await;
Ok(())
})
.await;
if let Err((Some(err), _)) = result {
return Err(err);
}
Ok(())
}
pub async fn run_test_with_result<A, S, T, E>(
app: A,
options: PrepareOptions,
test_fn: T,
) -> Result<(), (Option<crate::error::Error>, Option<E>)>
where
S: 'static + Send + Sync + Clone,
AppContext: FromRef<S>,
A: 'static + Send + Sync + App<S>,
T: std::ops::AsyncFnOnce(&TestAppState<A, S>) -> Result<(), E>,
E: std::error::Error,
{
let prepared = match app::prepare(app, options).await {
Ok(prepared) => prepared,
Err(err) => return Err((Some(err), None)),
};
let prepared = PreparedAppWithoutCli {
app: prepared.app,
state: prepared.state,
#[cfg(feature = "db-sql")]
migrator_registry: prepared.migrator_registry,
service_registry: prepared.service_registry,
lifecycle_handler_registry: prepared.lifecycle_handler_registry,
};
if let Err(err) = run::before_app(&prepared).await {
return Err((Some(err), None));
}
let pre_run_app_state = TestAppState {
app: prepared.app,
state: prepared.state.clone(),
service_registry: prepared.service_registry,
};
tracing::debug!("Starting test");
let context = AppContext::from_ref(&pre_run_app_state.state);
let (test_panic, test_result) = if context.config().testing.catch_panic {
let test_panic = AssertUnwindSafe(test_fn(&pre_run_app_state))
.catch_unwind()
.await;
(Some(test_panic), None)
} else {
let test_result = test_fn(&pre_run_app_state).await;
(None, Some(test_result))
};
tracing::debug!("Test complete");
let after_app_result =
run::after_app(&prepared.state, &prepared.lifecycle_handler_registry).await;
let test_result = if let Some(test_panic) = test_panic {
match test_panic {
Ok(ok) => Some(ok),
Err(err) => resume_unwind(err),
}
} else {
test_result
};
let test_result = if let Some(Err(err)) = test_result {
Some(err)
} else {
None
};
let after_app_result = after_app_result.err();
if after_app_result.is_some() || test_result.is_some() {
return Err((after_app_result, test_result));
}
Ok(())
}
pub async fn test_state<A, S>(app: A, config: AppConfig) -> RoadsterResult<S>
where
A: 'static + App<S>,
S: 'static + Send + Sync + Clone,
AppContext: FromRef<S>,
{
let state = prepare::build_state(&app, config).await?;
let prepared_without_cli = prepare::prepare_without_cli(app, state).await?;
Ok(prepared_without_cli.state)
}