//! Scenarios //! //! In general you will not need to interact with the [`Scenario`] type directly. //! Instead instances of it are constructed in the generated test functions and //! will be run automatically. use std::{cell::RefCell, marker::PhantomData, sync::Mutex}; use state::Container; use crate::step::ScenarioStep; use crate::types::{StepError, StepResult}; /// A context element is anything which can be used as a scenario step context. /// /// Contexts get called whenever the scenario steps occur so that they can do /// prep, cleanup, etc. It's important for authors of context element types to /// be aware that they won't always be called on scenario start and they will /// not be caught up the first time they are invoked for a step, simply expected /// to get on with life from their first use. pub trait ContextElement: Default + Send + 'static { /// A new context element was created. /// /// In order to permit elements which for example work on disk, this /// function will be invoked with the scenario's context to permit the /// context to register other contexts it might need, or to permit the /// creation of suitably named temporary directories, logging, etc. /// /// The scenario's title is available via [`scenario_context.title()`][title] /// /// [title]: [ScenarioContext::title] #[allow(unused_variables)] fn created(&mut self, scenario: &Scenario) { // Nothing by default } /// Scenario starts /// /// When a scenario starts, this function is called to permit setup. /// /// If this returns an error, scenario setup is stopped and `scenario_stops` /// will be called for anything which succeeded at startup. fn scenario_starts(&mut self) -> StepResult { Ok(()) } /// Scenario stops /// /// When a scenario finishes, this function is called to permit teardown. /// /// If this returns an error, and the scenario would otherwise have passed, /// then the error will be used. The first encountered error in stopping /// a scenario will be used, rather than the last. All contexts which /// succeeded at starting will be stopped. fn scenario_stops(&mut self) -> StepResult { Ok(()) } /// Entry to a step function /// /// In order to permit elements which for example work on disk, this /// function will be invoked with the step's name to permit the creation of /// suitably named temporary directories, logging, etc. /// /// The default implementation of this does nothing. /// /// Calls to this function *will* be paired with calls to the step exit /// function providing nothing panics or calls exit without unwinding. /// /// If you wish to be resilient to step functions panicing then you will /// need to be careful to cope with a new step being entered without a /// previous step exiting. Particularly if you're handing during cleanup /// of a failed scenario. /// /// If this returns an error then the step function is not run, nor is the /// corresponding `exit_step()` called. #[allow(unused_variables)] fn step_starts(&mut self, step_title: &str) -> StepResult { Ok(()) } /// Exit from a step function /// /// See [the `step_starts` function][ContextElement::step_starts] for most /// details of this. /// /// Any error returned from this will be masked if the step function itself /// returned an error. However if the step function succeeded then this /// function's error will make it out. #[allow(unused_variables)] fn step_stops(&mut self) -> StepResult { Ok(()) } } /// A scenario context wrapper for a particular context type struct ScenarioContextItem(Mutex); /// A type hook used purely in order to be able to look up contexts in the /// container in order to be able to iterate them during scenario execution struct ScenarioContextHook(PhantomData); impl ScenarioContextHook where C: ContextElement, { fn new() -> Self { Self(PhantomData::default()) } } /// A trait used to permit the holding of multiple hooks in one vector trait ScenarioContextHookKind { /// Start scenario fn scenario_starts(&self, contexts: &ScenarioContext) -> StepResult; /// Stop scenario fn scenario_stops(&self, contexts: &ScenarioContext) -> StepResult; /// Enter a step fn step_starts(&self, contexts: &ScenarioContext, step_name: &str) -> StepResult; /// Leave a step fn step_stops(&self, contexts: &ScenarioContext) -> StepResult; } impl ScenarioContextHookKind for ScenarioContextHook 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) } } /// A container for all scenario contexts /// /// This container allows the running of code within a given scenario context. pub struct ScenarioContext { title: String, inner: Container![], hooks: RefCell>>, } impl ScenarioContext { fn new(title: &str) -> Self { Self { title: title.to_string(), inner: ::new(), hooks: RefCell::new(Vec::new()), } } /// The title for this scenario fn title(&self) -> &str { &self.title } /// Ensure a context is registered pub(crate) fn register_context_type(&self) -> bool where C: ContextElement, { let sci: Option<&ScenarioContextItem> = 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::::new())); true } else { false } } /// With the extracted immutable context, run the function f. pub fn with(&self, func: F, defuse_poison: bool) -> Result where F: FnOnce(&C) -> Result, C: ContextElement, { self.with_mut(|c: &mut C| func(&*c), defuse_poison) } /// With the extracted mutable context, run the function f. pub fn with_mut(&self, func: F, defuse_poison: bool) -> Result where F: FnOnce(&mut C) -> Result, C: ContextElement, { let sci: &ScenarioContextItem = 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) } } /// The embodiment of a subplot scenario /// /// Scenario objects are built up by the generated test functions and then run /// to completion. In rare cases they may be built up and cached for reuse /// for example if a scenario is a subroutine. pub struct Scenario { contexts: ScenarioContext, steps: Vec<(ScenarioStep, Option)>, } impl Scenario { /// Create a new scenario with the given title pub fn new(title: &str) -> Self { Self { contexts: ScenarioContext::new(title), steps: Vec::new(), } } /// Retrieve the scenario title pub fn title(&self) -> &str { self.contexts.title() } /// Add a scenario step, with optional cleanup step function. pub fn add_step(&mut self, step: ScenarioStep, cleanup: Option) { step.register_contexts(self); if let Some(s) = cleanup.as_ref() { s.register_contexts(self) } self.steps.push((step, cleanup)); } /// Register a type with the scenario contexts pub fn register_context_type(&self) where C: ContextElement, { if self.contexts.register_context_type::() { self.contexts .with_mut( |c: &mut C| { c.created(self); Ok(()) }, false, ) .unwrap(); } } /// Run the scenario to completion. /// /// Running the scenario to completion requires running each step in turn. /// This will return the first encountered error, or unit if the scenario /// runs cleanly. /// /// # Panics /// /// If any of the cleanup functions error, this will immediately panic. /// pub fn run(self) -> Result<(), StepError> { // Firstly, we start all the contexts let mut ret = Ok(()); let mut highest_start = None; println!("Scenario Start: {}", 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); } println!( "*** Context hooks returned {}", if ret.is_ok() { "OK" } else { "Failure" } ); if ret.is_ok() { let mut highest = None; for (i, step) in self.steps.iter().map(|(step, _)| step).enumerate() { println!(" !!! Step {}", step.name()); let mut highest_prep = None; for (i, prep) in self.contexts.hooks.borrow().iter().enumerate() { let res = prep.step_starts(&self.contexts, step.name()); if res.is_err() { ret = res; break; } highest_prep = Some(i); } println!( " *** Context hooks returned {}", if ret.is_ok() { "OK" } else { "Failure" } ); if ret.is_ok() { println!(" >>> Run step function"); let res = step.call(&self.contexts, false); println!( " Step returned {}", if res.is_ok() { "OK" } else { "Failure" } ); if res.is_err() { ret = res; break; } highest = Some(i); } if let Some(n) = highest_prep { println!(" *** Unwinding step contexts"); 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 { println!(" *** Running cleanup functions"); for stepn in (0..=n).rev() { if let (_, Some(cleanup)) = &self.steps[stepn] { println!(" >>> Cleanup {}", cleanup.name()); let res = cleanup.call(&self.contexts, true); println!( " Cleanup returned {}", if res.is_ok() { "OK" } else { "Failure" } ); ret = ret.and(res); } } } } if let Some(n) = highest_start { println!("*** Running scenario closedown"); for hookn in (0..=n).rev() { let res = self.contexts.hooks.borrow()[hookn].scenario_stops(&self.contexts); ret = ret.and(res); } } println!( "<<< Scenario returns {}", if ret.is_ok() { "OK" } else { "Failure" } ); ret } }