use crate::context::ContextRegistry;
use crate::step::BoxedStep;
use crate::{ClientRegistry, Resources, Store};
use anyhow::Context;
use std::collections::HashMap;
use std::fmt;
pub type TestResult = Result<(), TestFailure>;
type EnvironmentHook<E> = Box<dyn Fn(&mut E) -> anyhow::Result<()> + Send + Sync>;
#[derive(thiserror::Error)]
pub enum TestFailure {
#[error("{0}")]
BuildFailure(#[from] BuildFailure),
#[error("{0}")]
PrepareFailed(#[from] PrepareFailed),
#[error("{0}")]
SetupFailed(#[from] SetupFailed),
#[error("{0}")]
StepFailed(#[from] StepFailed),
#[error("{0}")]
TeardownFailed(#[from] TeardownFailed),
}
impl fmt::Debug for TestFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
#[derive(Debug, thiserror::Error)]
pub struct BuildFailure(#[from] pub anyhow::Error);
impl fmt::Display for BuildFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to build the test: {}", self.0)
}
}
#[derive(Debug, thiserror::Error)]
pub struct PrepareFailed(#[from] pub anyhow::Error);
impl fmt::Display for PrepareFailed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to prepare the test environment: {}", self.0)
}
}
#[derive(Debug, thiserror::Error)]
pub struct SetupFailed(pub String);
impl fmt::Display for SetupFailed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to setup the test environment: {}", self.0)
}
}
#[derive(Debug, thiserror::Error)]
pub struct StepFailed {
pub error: Box<StepFailure>,
}
impl fmt::Display for StepFailed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
#[derive(Debug)]
pub struct StepFailure {
pub step: BoxedStep,
pub error: anyhow::Error,
}
impl fmt::Display for StepFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Step {:?} execution failed: {}", self.step, self.error)
}
}
#[derive(Debug, thiserror::Error)]
pub struct TeardownFailed(#[from] pub anyhow::Error);
impl fmt::Display for TeardownFailed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to teardown the test environment: {}", self.0)
}
}
#[derive(Debug)]
pub struct TestState {
pub clients: ClientRegistry,
pub store: Store,
pub resources: Resources,
}
pub trait Environment: Send + Sync {
fn setup(
&mut self,
configurations: HashMap<String, String>,
) -> impl std::future::Future<Output = anyhow::Result<()>> + Send;
fn teardown(&self) -> impl std::future::Future<Output = anyhow::Result<()>> + Send;
fn state(&self) -> TestState;
}
pub struct Test<E: Environment> {
pub setup_items: Vec<SetupItem>,
pub steps: Vec<BoxedStep>,
pub configurations: HashMap<String, String>,
pub context_registry: ContextRegistry,
pub environment_hooks: Vec<EnvironmentHook<E>>,
pub environment: E,
}
impl<E: Environment> Test<E> {
pub fn prepare(filter: log::LevelFilter, path: Option<&str>) -> Result<(), PrepareFailed> {
let _ = env_logger::builder()
.is_test(true)
.filter_level(filter)
.try_init();
if let Some(path) = path {
dotenvy::from_path(path)
.context("Failed to load test environment variables")
.map_err(PrepareFailed)?;
}
Ok(())
}
pub async fn run(self) -> TestResult {
let Test {
setup_items,
steps: test_steps,
configurations,
context_registry,
environment_hooks,
mut environment,
} = self;
environment
.setup(configurations)
.await
.map_err(BuildFailure)?;
for hook in &environment_hooks {
hook(&mut environment).map_err(BuildFailure)?;
}
let mut steps: Vec<BoxedStep> = Vec::new();
for item in setup_items.iter() {
match item {
SetupItem::Context(context_name) => {
let context_steps = context_registry
.resolve(context_name)
.ok_or_else(|| SetupFailed(format!("Unknown context: {context_name}")))?;
steps.extend(context_steps);
}
SetupItem::ContextWithArgs { name, args } => {
let context_steps = context_registry
.resolve_with_args(name, args)
.map_err(|error| SetupFailed(error.to_string()))?;
steps.extend(context_steps);
}
SetupItem::Step(step) => steps.push(step.clone()),
}
}
steps.extend(test_steps);
let state = environment.state();
for step in steps {
let step_for_error = step.clone();
step.execute(&state).await.map_err(|error| StepFailed {
error: Box::new(StepFailure {
step: step_for_error,
error,
}),
})?;
}
environment
.teardown()
.await
.map_err(|error| TestFailure::TeardownFailed(TeardownFailed(error)))
}
}
pub struct TestBuilder<E: Environment> {
setup_items: Vec<SetupItem>,
steps: Vec<BoxedStep>,
configurations: HashMap<String, String>,
context_registry: Option<ContextRegistry>,
environment_hooks: Vec<EnvironmentHook<E>>,
environment: Option<E>,
}
impl<E: Environment> Default for TestBuilder<E> {
fn default() -> Self {
Self::new()
}
}
impl<E: Environment> TestBuilder<E> {
pub fn new() -> Self {
Self {
setup_items: Vec::new(),
steps: Vec::new(),
configurations: HashMap::new(),
context_registry: None,
environment_hooks: Vec::new(),
environment: None,
}
}
pub fn step(mut self, step: impl Into<BoxedStep>) -> Self {
self.steps.push(step.into());
self
}
pub fn context(mut self, name: impl Into<String>) -> Self {
self.setup_items.push(SetupItem::Context(name.into()));
self
}
pub fn context_with_args(mut self, name: impl Into<String>, args: serde_json::Value) -> Self {
self.setup_items.push(SetupItem::ContextWithArgs {
name: name.into(),
args,
});
self
}
pub fn setup_step(mut self, step: impl Into<BoxedStep>) -> Self {
self.setup_items.push(SetupItem::Step(step.into()));
self
}
pub fn setup_steps<S: Into<BoxedStep>>(mut self, steps: Vec<S>) -> Self {
for step in steps {
self.setup_items.push(SetupItem::Step(step.into()));
}
self
}
pub fn contexts(mut self, registry: ContextRegistry) -> Self {
self.context_registry = Some(registry);
self
}
pub fn configuration(mut self, name: impl Into<String>, configuration: String) -> Self {
self.configurations.insert(name.into(), configuration);
self
}
pub fn environment(mut self, environment: E) -> Self {
self.environment = Some(environment);
self
}
pub fn configure_environment(
mut self,
hook: impl Fn(&mut E) -> anyhow::Result<()> + Send + Sync + 'static,
) -> Self {
self.environment_hooks.push(Box::new(hook));
self
}
pub fn build(self) -> Result<Test<E>, BuildFailure> {
if self.steps.is_empty() && self.setup_items.is_empty() {
return Err(BuildFailure(anyhow::anyhow!("No steps are set")));
}
let registry = self.context_registry.unwrap_or_default();
let environment = self
.environment
.ok_or_else(|| BuildFailure(anyhow::anyhow!("Environment is required")))?;
Ok(Test {
setup_items: self.setup_items,
steps: self.steps,
configurations: self.configurations,
context_registry: registry,
environment_hooks: self.environment_hooks,
environment,
})
}
}
#[derive(Debug, Clone)]
pub enum SetupItem {
Context(String),
ContextWithArgs {
name: String,
args: serde_json::Value,
},
Step(BoxedStep),
}