summaryrefslogtreecommitdiff
path: root/subplotlib/src/step.rs
blob: bbea249b9cce608a3fcce0627e5bc533b2235445 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//! 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<dyn Fn(&ScenarioContext, bool) -> StepResult>,
    reg: Box<dyn Fn(&Scenario)>,
}

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<F, R>(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<dyn Any + Send>) -> String {
        if let Some(msg) = err.downcast_ref::<&str>() {
            format!("step {} panic'd: {}", name, msg)
        } else if let Some(msg) = err.downcast_ref::<String>() {
            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);
    }
}