1use 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
16pub trait ContextElement: Debug + Default + Send + 'static {
24 #[allow(unused_variables)]
35 fn created(&mut self, scenario: &Scenario) {
36 }
38
39 fn scenario_starts(&mut self) -> StepResult {
46 Ok(())
47 }
48
49 fn scenario_stops(&mut self) -> StepResult {
58 Ok(())
59 }
60
61 #[allow(unused_variables)]
80 fn step_starts(&mut self, step_title: &str) -> StepResult {
81 Ok(())
82 }
83
84 #[allow(unused_variables)]
93 fn step_stops(&mut self) -> StepResult {
94 Ok(())
95 }
96}
97
98struct ScenarioContextItem<C>(Mutex<C>);
100
101struct ScenarioContextHook<C>(PhantomData<C>);
104
105impl<C> ScenarioContextHook<C>
106where
107 C: ContextElement,
108{
109 fn new() -> Self {
110 Self(PhantomData)
111 }
112}
113
114trait ScenarioContextHookKind {
116 fn scenario_starts(&self, contexts: &ScenarioContext) -> StepResult;
118
119 fn scenario_stops(&self, contexts: &ScenarioContext) -> StepResult;
121
122 fn step_starts(&self, contexts: &ScenarioContext, step_name: &str) -> StepResult;
124
125 fn step_stops(&self, contexts: &ScenarioContext) -> StepResult;
127
128 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
157pub 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 fn title(&self) -> &str {
227 &self.title
228 }
229
230 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 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 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
297pub struct Scenario {
318 contexts: ScenarioContext,
319 steps: Vec<(ScenarioStep, Option<ScenarioStep>)>,
320}
321
322impl Scenario {
323 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 pub fn title(&self) -> &str {
333 self.contexts.title()
334 }
335
336 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 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 pub fn run(self) -> Result<(), StepError> {
374 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!(
397 "{}: step {}: {}",
398 step.location(),
399 step.nr(),
400 step.step_text()
401 );
402 let mut highest_prep = None;
403 for (i, prep) in self.contexts.hooks.borrow().iter().enumerate() {
404 let res = prep.step_starts(&self.contexts, step.step_text());
405 if res.is_err() {
406 ret = res;
407 break;
408 }
409 highest_prep = Some(i);
410 }
411 if ret.is_err() {
412 println!("*** Context hooks returned failure",);
413 }
414 if ret.is_ok() {
415 let res = step.call(&self.contexts, false);
416 if res.is_err() {
417 ret = res;
418 break;
419 }
420 highest = Some(i);
421 }
422 if let Some(n) = highest_prep {
423 for hookn in (0..=n).rev() {
424 let res = self.contexts.hooks.borrow()[hookn].step_stops(&self.contexts);
425 ret = ret.and(res)
426 }
427 }
428 }
429 if let Some(n) = highest {
430 for stepn in (0..=n).rev() {
431 if let (_, Some(cleanup)) = &self.steps[stepn] {
432 println!(" cleanup {}: {}", cleanup.nr(), cleanup.step_text());
433 let res = cleanup.call(&self.contexts, true);
434 if res.is_err() {
435 println!("*** Cleanup returned failure",);
436 }
437 ret = ret.and(res);
438 }
439 }
440 }
441 }
442
443 if let Some(n) = highest_start {
444 for hookn in (0..=n).rev() {
445 let res = self.contexts.hooks.borrow()[hookn].scenario_stops(&self.contexts);
446 ret = ret.and(res);
447 }
448 }
449 println!(" return: {}", if ret.is_ok() { "OK" } else { "Failure" });
450 ret
451 }
452}