use core::fmt;
use std::fmt::Debug;
use std::{cell::RefCell, marker::PhantomData, sync::Mutex};
use state::TypeMap;
use crate::step::ScenarioStep;
use crate::types::{StepError, StepResult};
pub trait ContextElement: Debug + Default + Send + 'static {
#[allow(unused_variables)]
fn created(&mut self, scenario: &Scenario) {
}
fn scenario_starts(&mut self) -> StepResult {
Ok(())
}
fn scenario_stops(&mut self) -> StepResult {
Ok(())
}
#[allow(unused_variables)]
fn step_starts(&mut self, step_title: &str) -> StepResult {
Ok(())
}
#[allow(unused_variables)]
fn step_stops(&mut self) -> StepResult {
Ok(())
}
}
struct ScenarioContextItem<C>(Mutex<C>);
struct ScenarioContextHook<C>(PhantomData<C>);
impl<C> ScenarioContextHook<C>
where
C: ContextElement,
{
fn new() -> Self {
Self(PhantomData)
}
}
trait ScenarioContextHookKind {
fn scenario_starts(&self, contexts: &ScenarioContext) -> StepResult;
fn scenario_stops(&self, contexts: &ScenarioContext) -> StepResult;
fn step_starts(&self, contexts: &ScenarioContext, step_name: &str) -> StepResult;
fn step_stops(&self, contexts: &ScenarioContext) -> StepResult;
fn debug(&self, contexts: &ScenarioContext, dc: &mut DebuggedContext, alternate: bool);
}
impl<C> ScenarioContextHookKind for ScenarioContextHook<C>
where
C: ContextElement,
{
fn scenario_starts(&self, contexts: &ScenarioContext) -> StepResult {
contexts.with_mut(|c: &mut C| c.scenario_starts(), false)
}
fn scenario_stops(&self, contexts: &ScenarioContext) -> StepResult {
contexts.with_mut(|c: &mut C| c.scenario_stops(), true)
}
fn step_starts(&self, contexts: &ScenarioContext, step_name: &str) -> StepResult {
contexts.with_mut(|c: &mut C| c.step_starts(step_name), false)
}
fn step_stops(&self, contexts: &ScenarioContext) -> StepResult {
contexts.with_mut(|c: &mut C| c.step_stops(), true)
}
fn debug(&self, contexts: &ScenarioContext, dc: &mut DebuggedContext, alternate: bool) {
contexts.with_generic(|c: &C| dc.add(c, alternate));
}
}
pub struct ScenarioContext {
title: String,
location: &'static str,
inner: TypeMap![],
hooks: RefCell<Vec<Box<dyn ScenarioContextHookKind>>>,
}
#[derive(Default)]
struct DebuggedContext {
body: Vec<String>,
}
impl DebuggedContext {
fn add<C>(&mut self, obj: &C, alternate: bool)
where
C: Debug,
{
let body = if alternate {
format!("{obj:#?}")
} else {
format!("{obj:?}")
};
self.body.push(body);
}
}
struct DebugContextString<'a>(&'a str);
impl Debug for DebugContextString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
impl Debug for DebuggedContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.body.iter().map(|s| DebugContextString(s)))
.finish()
}
}
impl Debug for ScenarioContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut contexts = DebuggedContext::default();
for hook in self.hooks.borrow().iter() {
hook.debug(self, &mut contexts, f.alternate());
}
f.debug_struct("ScenarioContext")
.field("title", &self.title)
.field("contexts", &contexts)
.finish()
}
}
impl ScenarioContext {
fn new(title: &str, location: &'static str) -> Self {
Self {
title: title.to_string(),
location,
inner: <TypeMap![]>::new(),
hooks: RefCell::new(Vec::new()),
}
}
fn title(&self) -> &str {
&self.title
}
pub(crate) fn register_context_type<C>(&self) -> bool
where
C: ContextElement,
{
let sci: Option<&ScenarioContextItem<C>> = self.inner.try_get();
if sci.is_none() {
let ctx = ScenarioContextItem(Mutex::new(C::default()));
self.inner.set(ctx);
self.hooks
.borrow_mut()
.push(Box::new(ScenarioContextHook::<C>::new()));
true
} else {
false
}
}
fn with_generic<C, F>(&self, func: F)
where
F: FnOnce(&C),
C: ContextElement,
{
let sci: &ScenarioContextItem<C> = self
.inner
.try_get()
.expect("Scenario Context item not initialised");
let lock = match sci.0.lock() {
Ok(lock) => lock,
Err(pe) => pe.into_inner(),
};
func(&lock)
}
pub fn with<C, F, R>(&self, func: F, defuse_poison: bool) -> Result<R, StepError>
where
F: FnOnce(&C) -> Result<R, StepError>,
C: ContextElement,
{
self.with_mut(|c: &mut C| func(&*c), defuse_poison)
}
pub fn with_mut<C, F, R>(&self, func: F, defuse_poison: bool) -> Result<R, StepError>
where
F: FnOnce(&mut C) -> Result<R, StepError>,
C: ContextElement,
{
let sci: &ScenarioContextItem<C> = self
.inner
.try_get()
.ok_or("required context type not registered with scenario")?;
let mut lock = match sci.0.lock() {
Ok(lock) => lock,
Err(pe) => {
if defuse_poison {
pe.into_inner()
} else {
return Err("context poisoned by panic".into());
}
}
};
func(&mut lock)
}
}
pub struct Scenario {
contexts: ScenarioContext,
steps: Vec<(ScenarioStep, Option<ScenarioStep>)>,
}
impl Scenario {
pub fn new(title: &str, location: &'static str) -> Self {
Self {
contexts: ScenarioContext::new(title, location),
steps: Vec::new(),
}
}
pub fn title(&self) -> &str {
self.contexts.title()
}
pub fn add_step(&mut self, step: ScenarioStep, cleanup: Option<ScenarioStep>) {
step.register_contexts(self);
if let Some(s) = cleanup.as_ref() {
s.register_contexts(self)
}
self.steps.push((step, cleanup));
}
pub fn register_context_type<C>(&self)
where
C: ContextElement,
{
if self.contexts.register_context_type::<C>() {
self.contexts
.with_mut(
|c: &mut C| {
c.created(self);
Ok(())
},
false,
)
.unwrap();
}
}
pub fn run(self) -> Result<(), StepError> {
let mut ret = Ok(());
let mut highest_start = None;
println!(
"{}: scenario: {}",
self.contexts.location,
self.contexts.title()
);
for (i, hook) in self.contexts.hooks.borrow().iter().enumerate() {
let res = hook.scenario_starts(&self.contexts);
if res.is_err() {
ret = res;
break;
}
highest_start = Some(i);
}
if ret.is_err() {
println!("*** Context hooks returned failure",);
}
if ret.is_ok() {
let mut highest = None;
for (i, step) in self.steps.iter().map(|(step, _)| step).enumerate() {
println!(
"{}: step {}: {}",
step.location(),
step.nr(),
step.step_text()
);
let mut highest_prep = None;
for (i, prep) in self.contexts.hooks.borrow().iter().enumerate() {
let res = prep.step_starts(&self.contexts, step.step_text());
if res.is_err() {
ret = res;
break;
}
highest_prep = Some(i);
}
if ret.is_err() {
println!("*** Context hooks returned failure",);
}
if ret.is_ok() {
let res = step.call(&self.contexts, false);
if res.is_err() {
ret = res;
break;
}
highest = Some(i);
}
if let Some(n) = highest_prep {
for hookn in (0..=n).rev() {
let res = self.contexts.hooks.borrow()[hookn].step_stops(&self.contexts);
ret = ret.and(res)
}
}
}
if let Some(n) = highest {
for stepn in (0..=n).rev() {
if let (_, Some(cleanup)) = &self.steps[stepn] {
println!(" cleanup {}: {}", cleanup.nr(), cleanup.step_text());
let res = cleanup.call(&self.contexts, true);
if res.is_err() {
println!("*** Cleanup returned failure",);
}
ret = ret.and(res);
}
}
}
}
if let Some(n) = highest_start {
for hookn in (0..=n).rev() {
let res = self.contexts.hooks.borrow()[hookn].scenario_stops(&self.contexts);
ret = ret.and(res);
}
}
println!(" return: {}", if ret.is_ok() { "OK" } else { "Failure" });
ret
}
}