//! Scenario steps //! //! In general you will not need to interact with these types, they're simply //! here to provide wrappers to make the scenarios easier to work with. //! use std::any::Any; use std::panic::{catch_unwind, AssertUnwindSafe}; use crate::scenario::{Scenario, ScenarioContext}; use crate::types::StepResult; /// A ScenarioStep is one step in a scenario. /// /// In essence, a scenario step is a named closure. Its name can be used when /// reporting an error encountered in running a scenario. /// /// Scenario steps are typically constructed from step builders, rather than /// directly. This permits the step builder to correctly register context types /// etc. /// /// ``` /// # use subplotlib::prelude::*; /// /// let step = ScenarioStep::new( /// "when everything works".to_string(), |ctx, ok| Ok(()), |scen| () /// ); /// ``` pub struct ScenarioStep { step_text: String, func: Box StepResult>, reg: Box, } impl ScenarioStep { /// Create a new scenario step taking the scenario context /// /// This is used to construct a scenario step from a function which /// takes the scenario context container. This will generally be /// called from the generated build method for the step. pub fn new(step_text: String, func: F, reg: R) -> Self where F: Fn(&ScenarioContext, bool) -> StepResult + 'static, R: Fn(&Scenario) + 'static, { Self { step_text, func: Box::new(func), reg: Box::new(reg), } } /// Attempt to render a message. /// If something panics with a type other than a static string or /// a formatted string then we won't be able to render it sadly. fn render_panic(name: &str, err: Box) -> String { if let Some(msg) = err.downcast_ref::<&str>() { format!("step {} panic'd: {}", name, msg) } else if let Some(msg) = err.downcast_ref::() { format!("step {} panic'd: {}", name, msg) } else { format!("step {} panic'd", name) } } /// Call the step function /// /// This simply calls the encased step function pub fn call(&self, context: &ScenarioContext, defuse_poison: bool) -> StepResult { // Note, panic here will be absorbed and so there's a risk that // subsequent step calls may not be sound. There's not a lot we can // do to ensure things are good except try. let func = AssertUnwindSafe(|| (*self.func)(context, defuse_poison)); catch_unwind(func).map_err(|e| Self::render_panic(self.step_text(), e))? } /// Return the full text of this step pub fn step_text(&self) -> &str { &self.step_text } /// Register any context types needed by this step pub(crate) fn register_contexts(&self, scenario: &Scenario) { (*self.reg)(scenario); } }