subplotlib/
scenario.rs

1//! Scenarios
2//!
3//! In general you will not need to interact with the [`Scenario`] type directly.
4//! Instead instances of it are constructed in the generated test functions and
5//! will be run automatically.
6
7use core::fmt;
8use std::fmt::Debug;
9use std::{cell::RefCell, marker::PhantomData, sync::Mutex};
10
11use state::TypeMap;
12
13use crate::step::ScenarioStep;
14use crate::types::{StepError, StepResult};
15
16/// A context element is anything which can be used as a scenario step context.
17///
18/// Contexts get called whenever the scenario steps occur so that they can do
19/// prep, cleanup, etc.  It's important for authors of context element types to
20/// be aware that they won't always be called on scenario start and they will
21/// not be caught up the first time they are invoked for a step, simply expected
22/// to get on with life from their first use.
23pub trait ContextElement: Debug + Default + Send + 'static {
24    /// A new context element was created.
25    ///
26    /// In order to permit elements which for example work on disk, this
27    /// function will be invoked with the scenario's context to permit the
28    /// context to register other contexts it might need, or to permit the
29    /// creation of suitably named temporary directories, logging, etc.
30    ///
31    /// The scenario's title is available via [`scenario_context.title()`][title]
32    ///
33    /// [title]: [ScenarioContext::title]
34    #[allow(unused_variables)]
35    fn created(&mut self, scenario: &Scenario) {
36        // Nothing by default
37    }
38
39    /// Scenario starts
40    ///
41    /// When a scenario starts, this function is called to permit setup.
42    ///
43    /// If this returns an error, scenario setup is stopped and `scenario_stops`
44    /// will be called for anything which succeeded at startup.
45    fn scenario_starts(&mut self) -> StepResult {
46        Ok(())
47    }
48
49    /// Scenario stops
50    ///
51    /// When a scenario finishes, this function is called to permit teardown.
52    ///
53    /// If this returns an error, and the scenario would otherwise have passed,
54    /// then the error will be used.  The first encountered error in stopping
55    /// a scenario will be used, rather than the last.  All contexts which
56    /// succeeded at starting will be stopped.
57    fn scenario_stops(&mut self) -> StepResult {
58        Ok(())
59    }
60
61    /// Entry to a step function
62    ///
63    /// In order to permit elements which for example work on disk, this
64    /// function will be invoked with the step's name to permit the creation of
65    /// suitably named temporary directories, logging, etc.
66    ///
67    /// The default implementation of this does nothing.
68    ///
69    /// Calls to this function *will* be paired with calls to the step exit
70    /// function providing nothing panics or calls exit without unwinding.
71    ///
72    /// If you wish to be resilient to step functions panicing then you will
73    /// need to be careful to cope with a new step being entered without a
74    /// previous step exiting.  Particularly if you're handing during cleanup
75    /// of a failed scenario.
76    ///
77    /// If this returns an error then the step function is not run, nor is the
78    /// corresponding `exit_step()` called.
79    #[allow(unused_variables)]
80    fn step_starts(&mut self, step_title: &str) -> StepResult {
81        Ok(())
82    }
83
84    /// Exit from a step function
85    ///
86    /// See [the `step_starts` function][ContextElement::step_starts] for most
87    /// details of this.
88    ///
89    /// Any error returned from this will be masked if the step function itself
90    /// returned an error.  However if the step function succeeded then this
91    /// function's error will make it out.
92    #[allow(unused_variables)]
93    fn step_stops(&mut self) -> StepResult {
94        Ok(())
95    }
96}
97
98/// A scenario context wrapper for a particular context type
99struct ScenarioContextItem<C>(Mutex<C>);
100
101/// A type hook used purely in order to be able to look up contexts in the
102/// container in order to be able to iterate them during scenario execution
103struct ScenarioContextHook<C>(PhantomData<C>);
104
105impl<C> ScenarioContextHook<C>
106where
107    C: ContextElement,
108{
109    fn new() -> Self {
110        Self(PhantomData)
111    }
112}
113
114/// A trait used to permit the holding of multiple hooks in one vector
115trait ScenarioContextHookKind {
116    /// Start scenario
117    fn scenario_starts(&self, contexts: &ScenarioContext) -> StepResult;
118
119    /// Stop scenario
120    fn scenario_stops(&self, contexts: &ScenarioContext) -> StepResult;
121
122    /// Enter a step
123    fn step_starts(&self, contexts: &ScenarioContext, step_name: &str) -> StepResult;
124
125    /// Leave a step
126    fn step_stops(&self, contexts: &ScenarioContext) -> StepResult;
127
128    /// Produce your debug output
129    fn debug(&self, contexts: &ScenarioContext, dc: &mut DebuggedContext, alternate: bool);
130}
131
132impl<C> ScenarioContextHookKind for ScenarioContextHook<C>
133where
134    C: ContextElement,
135{
136    fn scenario_starts(&self, contexts: &ScenarioContext) -> StepResult {
137        contexts.with_mut(|c: &mut C| c.scenario_starts(), false)
138    }
139
140    fn scenario_stops(&self, contexts: &ScenarioContext) -> StepResult {
141        contexts.with_mut(|c: &mut C| c.scenario_stops(), true)
142    }
143
144    fn step_starts(&self, contexts: &ScenarioContext, step_name: &str) -> StepResult {
145        contexts.with_mut(|c: &mut C| c.step_starts(step_name), false)
146    }
147
148    fn step_stops(&self, contexts: &ScenarioContext) -> StepResult {
149        contexts.with_mut(|c: &mut C| c.step_stops(), true)
150    }
151
152    fn debug(&self, contexts: &ScenarioContext, dc: &mut DebuggedContext, alternate: bool) {
153        contexts.with_generic(|c: &C| dc.add(c, alternate));
154    }
155}
156
157/// A container for all scenario contexts
158///
159/// This container allows the running of code within a given scenario context.
160pub struct ScenarioContext {
161    title: String,
162    location: &'static str,
163    inner: TypeMap![],
164    hooks: RefCell<Vec<Box<dyn ScenarioContextHookKind>>>,
165}
166
167#[derive(Default)]
168struct DebuggedContext {
169    body: Vec<String>,
170}
171
172impl DebuggedContext {
173    fn add<C>(&mut self, obj: &C, alternate: bool)
174    where
175        C: Debug,
176    {
177        let body = if alternate {
178            format!("{obj:#?}")
179        } else {
180            format!("{obj:?}")
181        };
182        self.body.push(body);
183    }
184}
185
186struct DebugContextString<'a>(&'a str);
187
188impl Debug for DebugContextString<'_> {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        f.write_str(self.0)
191    }
192}
193
194impl Debug for DebuggedContext {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.debug_list()
197            .entries(self.body.iter().map(|s| DebugContextString(s)))
198            .finish()
199    }
200}
201
202impl Debug for ScenarioContext {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        let mut contexts = DebuggedContext::default();
205        for hook in self.hooks.borrow().iter() {
206            hook.debug(self, &mut contexts, f.alternate());
207        }
208        f.debug_struct("ScenarioContext")
209            .field("title", &self.title)
210            .field("contexts", &contexts)
211            .finish()
212    }
213}
214
215impl ScenarioContext {
216    fn new(title: &str, location: &'static str) -> Self {
217        Self {
218            title: title.to_string(),
219            location,
220            inner: <TypeMap![]>::new(),
221            hooks: RefCell::new(Vec::new()),
222        }
223    }
224
225    /// The title for this scenario
226    fn title(&self) -> &str {
227        &self.title
228    }
229
230    /// Ensure a context is registered
231    pub(crate) fn register_context_type<C>(&self) -> bool
232    where
233        C: ContextElement,
234    {
235        let sci: Option<&ScenarioContextItem<C>> = self.inner.try_get();
236        if sci.is_none() {
237            let ctx = ScenarioContextItem(Mutex::new(C::default()));
238            self.inner.set(ctx);
239            self.hooks
240                .borrow_mut()
241                .push(Box::new(ScenarioContextHook::<C>::new()));
242            true
243        } else {
244            false
245        }
246    }
247
248    fn with_generic<C, F>(&self, func: F)
249    where
250        F: FnOnce(&C),
251        C: ContextElement,
252    {
253        let sci: &ScenarioContextItem<C> = self
254            .inner
255            .try_get()
256            .expect("Scenario Context item not initialised");
257        let lock = match sci.0.lock() {
258            Ok(lock) => lock,
259            Err(pe) => pe.into_inner(),
260        };
261        func(&lock)
262    }
263
264    /// With the extracted immutable context, run the function f.
265    pub fn with<C, F, R>(&self, func: F, defuse_poison: bool) -> Result<R, StepError>
266    where
267        F: FnOnce(&C) -> Result<R, StepError>,
268        C: ContextElement,
269    {
270        self.with_mut(|c: &mut C| func(&*c), defuse_poison)
271    }
272
273    /// With the extracted mutable context, run the function f.
274    pub fn with_mut<C, F, R>(&self, func: F, defuse_poison: bool) -> Result<R, StepError>
275    where
276        F: FnOnce(&mut C) -> Result<R, StepError>,
277        C: ContextElement,
278    {
279        let sci: &ScenarioContextItem<C> = self
280            .inner
281            .try_get()
282            .ok_or("required context type not registered with scenario")?;
283        let mut lock = match sci.0.lock() {
284            Ok(lock) => lock,
285            Err(pe) => {
286                if defuse_poison {
287                    pe.into_inner()
288                } else {
289                    return Err("context poisoned by panic".into());
290                }
291            }
292        };
293        func(&mut lock)
294    }
295}
296
297/// The embodiment of a subplot scenario
298///
299/// Scenario objects are built up by the generated test functions and then run
300/// to completion.  In rare cases they may be built up and cached for reuse
301/// for example if a scenario is a subroutine.
302///
303/// Scenarios are built from steps in sequence, and then can be run.
304///
305/// ```
306/// # use subplotlib::prelude::*;
307///
308/// let mut scenario = Scenario::new("example scenario", "unknown");
309///
310/// let run_step = subplotlib::steplibrary::runcmd::run::Builder::default()
311///     .argv0("true")
312///     .args("")
313///     .build("when I run true".to_string(), "unknown");
314/// scenario.add_step(run_step, None);
315///
316/// ```
317pub struct Scenario {
318    contexts: ScenarioContext,
319    steps: Vec<(ScenarioStep, Option<ScenarioStep>)>,
320}
321
322impl Scenario {
323    /// Create a new scenario with the given title
324    pub fn new(title: &str, location: &'static str) -> Self {
325        Self {
326            contexts: ScenarioContext::new(title, location),
327            steps: Vec::new(),
328        }
329    }
330
331    /// Retrieve the scenario title
332    pub fn title(&self) -> &str {
333        self.contexts.title()
334    }
335
336    /// Add a scenario step, with optional cleanup step function.
337    pub fn add_step(&mut self, step: ScenarioStep, cleanup: Option<ScenarioStep>) {
338        step.register_contexts(self);
339        if let Some(s) = cleanup.as_ref() {
340            s.register_contexts(self)
341        }
342        self.steps.push((step, cleanup));
343    }
344
345    /// Register a type with the scenario contexts
346    pub fn register_context_type<C>(&self)
347    where
348        C: ContextElement,
349    {
350        if self.contexts.register_context_type::<C>() {
351            self.contexts
352                .with_mut(
353                    |c: &mut C| {
354                        c.created(self);
355                        Ok(())
356                    },
357                    false,
358                )
359                .unwrap();
360        }
361    }
362
363    /// Run the scenario to completion.
364    ///
365    /// Running the scenario to completion requires running each step in turn.
366    /// This will return the first encountered error, or unit if the scenario
367    /// runs cleanly.
368    ///
369    /// # Panics
370    ///
371    /// If any of the cleanup functions error, this will immediately panic.
372    ///
373    pub fn run(self) -> Result<(), StepError> {
374        // Firstly, we start all the contexts
375        let mut ret = Ok(());
376        let mut highest_start = None;
377        println!(
378            "{}: scenario: {}",
379            self.contexts.location,
380            self.contexts.title()
381        );
382        for (i, hook) in self.contexts.hooks.borrow().iter().enumerate() {
383            let res = hook.scenario_starts(&self.contexts);
384            if res.is_err() {
385                ret = res;
386                break;
387            }
388            highest_start = Some(i);
389        }
390        if ret.is_err() {
391            println!("*** Context hooks returned failure",);
392        }
393        if ret.is_ok() {
394            let mut highest = None;
395            for (i, step) in self.steps.iter().map(|(step, _)| step).enumerate() {
396                println!("{}:   step: {}", step.location(), step.step_text());
397                let mut highest_prep = None;
398                for (i, prep) in self.contexts.hooks.borrow().iter().enumerate() {
399                    let res = prep.step_starts(&self.contexts, step.step_text());
400                    if res.is_err() {
401                        ret = res;
402                        break;
403                    }
404                    highest_prep = Some(i);
405                }
406                if ret.is_err() {
407                    println!("*** Context hooks returned failure",);
408                }
409                if ret.is_ok() {
410                    let res = step.call(&self.contexts, false);
411                    if res.is_err() {
412                        ret = res;
413                        break;
414                    }
415                    highest = Some(i);
416                }
417                if let Some(n) = highest_prep {
418                    for hookn in (0..=n).rev() {
419                        let res = self.contexts.hooks.borrow()[hookn].step_stops(&self.contexts);
420                        ret = ret.and(res)
421                    }
422                }
423            }
424            if let Some(n) = highest {
425                for stepn in (0..=n).rev() {
426                    if let (_, Some(cleanup)) = &self.steps[stepn] {
427                        println!("  cleanup: {}", cleanup.step_text());
428                        let res = cleanup.call(&self.contexts, true);
429                        if res.is_err() {
430                            println!("*** Cleanup returned failure",);
431                        }
432                        ret = ret.and(res);
433                    }
434                }
435            }
436        }
437
438        if let Some(n) = highest_start {
439            for hookn in (0..=n).rev() {
440                let res = self.contexts.hooks.borrow()[hookn].scenario_stops(&self.contexts);
441                ret = ret.and(res);
442            }
443        }
444        println!("  return: {}", if ret.is_ok() { "OK" } else { "Failure" });
445        ret
446    }
447}