From 3fd40d39596087cf346958381d263879d6e11cb4 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 14 Oct 2021 15:59:10 +0300 Subject: [PATCH 1/9] Add before and after hooks without propagating errors --- src/cucumber.rs | 79 ++++++++--- src/runner/basic.rs | 333 +++++++++++++++++++++++++++++++++++++------- tests/wait.rs | 19 ++- 3 files changed, 364 insertions(+), 67 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index c80163bd..c4e401d4 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -21,7 +21,7 @@ use std::{ }; use clap::Clap as _; -use futures::StreamExt as _; +use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; use crate::{ @@ -774,22 +774,9 @@ where I: AsRef, { fn default() -> Self { - let which: runner::basic::WhichScenarioFn = |_, _, scenario| { - scenario - .tags - .iter() - .any(|tag| tag == "serial") - .then(|| ScenarioType::Serial) - .unwrap_or(ScenarioType::Concurrent) - }; - Cucumber::custom() .with_parser(parser::Basic::new()) - .with_runner( - runner::Basic::custom() - .which_scenario(which) - .max_concurrent_scenarios(64), - ) + .with_runner(runner::Basic::default()) .with_writer(writer::Basic::new().normalized().summarized()) } } @@ -840,7 +827,7 @@ impl Cucumber { } } -impl Cucumber, Wr> { +impl Cucumber, Wr> { /// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will /// be limited. /// @@ -864,7 +851,7 @@ impl Cucumber, Wr> { pub fn which_scenario( self, func: Which, - ) -> Cucumber, Wr> + ) -> Cucumber, Wr> where Which: Fn( &gherkin::Feature, @@ -888,6 +875,64 @@ impl Cucumber, Wr> { } } + /// TODO + #[must_use] + pub fn before( + self, + func: Before, + ) -> Cucumber, Wr> + where + Before: for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, + { + let Self { + parser, + runner, + writer, + .. + } = self; + Cucumber { + parser, + runner: runner.before(func), + writer, + _world: PhantomData, + _parser_input: PhantomData, + } + } + + /// TODO + #[must_use] + pub fn after( + self, + func: After, + ) -> Cucumber, Wr> + where + After: for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, + { + let Self { + parser, + runner, + writer, + .. + } = self; + Cucumber { + parser, + runner: runner.after(func), + writer, + _world: PhantomData, + _parser_input: PhantomData, + } + } + /// Replaces [`Collection`] of [`Step`]s. /// /// [`Collection`]: step::Collection diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 12bb1725..4ac4b11e 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -24,7 +24,7 @@ use std::{ use futures::{ channel::mpsc, - future::{self, Either}, + future::{self, Either, LocalBoxFuture}, lock::Mutex, pin_mut, stream::{self, LocalBoxStream}, @@ -66,7 +66,12 @@ pub enum ScenarioType { /// /// [1]: Runner#order-guarantees /// [`Scenario`]: gherkin::Scenario -pub struct Basic { +pub struct Basic< + World, + F = WhichScenarioFn, + Before = HookFn, + After = HookFn, +> { /// Optional number of concurrently executed [`Scenario`]s. /// /// [`Scenario`]: gherkin::Scenario @@ -84,6 +89,12 @@ pub struct Basic { /// [`Serial`]: ScenarioType::Serial /// [`Scenario`]: gherkin::Scenario which_scenario: F, + + /// TODO + before_hook: Option, + + /// TODO + after_hook: Option, } /// Alias for [`fn`] used to determine whether a [`Scenario`] is [`Concurrent`] @@ -98,6 +109,19 @@ pub type WhichScenarioFn = fn( &gherkin::Scenario, ) -> ScenarioType; +/// TODO +pub type HookFn = for<'a> fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut World, +) -> LocalBoxFuture<'a, ()>; + +enum Hook { + Before, + After, +} + // Implemented manually to omit redundant trait bounds on `World` and to omit // outputting `F`. impl fmt::Debug for Basic { @@ -117,11 +141,34 @@ impl Basic { max_concurrent_scenarios: None, steps: step::Collection::new(), which_scenario: (), + before_hook: None, + after_hook: None, + } + } +} + +impl Default for Basic { + fn default() -> Self { + let which: WhichScenarioFn = |_, _, scenario| { + scenario + .tags + .iter() + .any(|tag| tag == "serial") + .then(|| ScenarioType::Serial) + .unwrap_or(ScenarioType::Concurrent) + }; + + Self { + max_concurrent_scenarios: Some(64), + steps: step::Collection::new(), + which_scenario: which, + before_hook: None, + after_hook: None, } } } -impl Basic { +impl Basic { /// If `max` is [`Some`], then number of concurrently executed [`Scenario`]s /// will be limited. /// @@ -142,9 +189,9 @@ impl Basic { /// [`Serial`]: ScenarioType::Serial /// [`Scenario`]: gherkin::Scenario #[must_use] - pub fn which_scenario(self, func: Which) -> Basic + pub fn which_scenario(self, func: F) -> Basic where - Which: Fn( + F: Fn( &gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario, @@ -154,12 +201,70 @@ impl Basic { let Self { max_concurrent_scenarios, steps, + before_hook, + after_hook, .. } = self; Basic { max_concurrent_scenarios, steps, which_scenario: func, + before_hook, + after_hook, + } + } + + /// TODO + #[must_use] + pub fn before(self, func: Func) -> Basic + where + Func: for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut World, + ) -> LocalBoxFuture<'a, ()>, + { + let Self { + max_concurrent_scenarios, + steps, + which_scenario, + after_hook, + .. + } = self; + Basic { + max_concurrent_scenarios, + steps, + which_scenario, + before_hook: Some(func), + after_hook, + } + } + + /// TODO + #[must_use] + pub fn after(self, func: Func) -> Basic + where + Func: for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut World, + ) -> LocalBoxFuture<'a, ()>, + { + let Self { + max_concurrent_scenarios, + steps, + which_scenario, + before_hook, + .. + } = self; + Basic { + max_concurrent_scenarios, + steps, + which_scenario, + before_hook, + after_hook: Some(func), } } @@ -200,7 +305,7 @@ impl Basic { } } -impl Runner for Basic +impl Runner for Basic where W: World, Which: Fn( @@ -209,6 +314,20 @@ where &gherkin::Scenario, ) -> ScenarioType + 'static, + Before: 'static + + for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, + After: 'static + + for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, { type EventStream = LocalBoxStream<'static, parser::Result>>; @@ -221,6 +340,8 @@ where max_concurrent_scenarios, steps, which_scenario, + before_hook, + after_hook, } = self; let buffer = Features::default(); @@ -232,7 +353,14 @@ where which_scenario, sender.clone(), ); - let execute = execute(buffer, max_concurrent_scenarios, steps, sender); + let execute = execute( + buffer, + max_concurrent_scenarios, + steps, + sender, + before_hook, + after_hook, + ); stream::select( receiver.map(Either::Left), @@ -281,12 +409,30 @@ async fn insert_features( /// Retrieves [`Feature`]s and executes them. /// /// [`Feature`]: gherkin::Feature -async fn execute( +async fn execute( features: Features, max_concurrent_scenarios: Option, collection: step::Collection, sender: mpsc::UnboundedSender>>, -) { + before_hook: Option, + after_hook: Option, +) where + W: World, + Before: 'static + + for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, + After: 'static + + for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, +{ // Those panic hook shenanigans are done to avoid console messages like // "thread 'main' panicked at ..." // @@ -298,7 +444,8 @@ async fn execute( let hook = panic::take_hook(); panic::set_hook(Box::new(|_| {})); - let mut executor = Executor::new(collection, sender); + let mut executor = + Executor::new(collection, before_hook, after_hook, sender); executor.send(event::Cucumber::Started); @@ -334,7 +481,7 @@ async fn execute( /// completion. /// /// [`Feature`]: gherkin::Feature. -struct Executor { +struct Executor { /// Number of finished [`Scenario`]s of [`Feature`]. /// /// [`Feature`]: gherkin::Feature @@ -357,22 +504,48 @@ struct Executor { /// [`Step`]: step::Step collection: step::Collection, + /// TODO + before_hook: Option, + + /// TODO + after_hook: Option, + /// Sender for notifying state of [`Feature`]s completion. /// /// [`Feature`]: gherkin::Feature sender: mpsc::UnboundedSender>>, } -impl Executor { +impl Executor +where + Before: 'static + + for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, + After: 'static + + for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, +{ /// Creates a new [`Executor`]. fn new( collection: step::Collection, + before_hook: Option, + after_hook: Option, sender: mpsc::UnboundedSender>>, ) -> Self { Self { features_scenarios_count: HashMap::new(), rule_scenarios_count: HashMap::new(), collection, + before_hook, + after_hook, sender, } } @@ -390,29 +563,34 @@ impl Executor { /// [`Scenario`]: gherkin::Scenario async fn run_scenario( &self, - feature: Arc, - rule: Option>, - scenario: Arc, + f: Arc, + r: Option>, + s: Arc, ) { + use event::{ + Cucumber, + Scenario::{Finished, Started}, + }; + let ok = |e: fn(Arc) -> event::Scenario| { - let (f, r, s) = (&feature, &rule, &scenario); + let (f, r, s) = (&f, &r, &s); move |step| { let (f, r, s) = (f.clone(), r.clone(), s.clone()); - event::Cucumber::scenario(f, r, s, e(step)) + Cucumber::scenario(f, r, s, e(step)) } }; let ok_capt = |e: fn(Arc, _) -> event::Scenario| { - let (f, r, s) = (&feature, &rule, &scenario); + let (f, r, s) = (&f, &r, &s); move |step, captures| { let (f, r, s) = (f.clone(), r.clone(), s.clone()); - event::Cucumber::scenario(f, r, s, e(step, captures)) + Cucumber::scenario(f, r, s, e(step, captures)) } }; let err = |e: fn(Arc, _, _, _) -> event::Scenario| { - let (f, r, s) = (&feature, &rule, &scenario); + let (f, r, s) = (&f, &r, &s); move |step, captures, w, info| { let (f, r, s) = (f.clone(), r.clone(), s.clone()); - event::Cucumber::scenario(f, r, s, e(step, captures, w, info)) + Cucumber::scenario(f, r, s, e(step, captures, w, info)) } }; @@ -432,15 +610,14 @@ impl Executor { event::Scenario::step_failed, ); - self.send(event::Cucumber::scenario( - feature.clone(), - rule.clone(), - scenario.clone(), - event::Scenario::Started, - )); + self.send(Cucumber::scenario(f.clone(), r.clone(), s.clone(), Started)); let res = async { - let feature_background = feature + let before_hook = self + .run_hook(Hook::Before, None, &f, r.as_ref(), &s) + .await?; + + let feature_background = f .background .as_ref() .map(|b| b.steps.iter().map(|s| Arc::new(s.clone()))) @@ -449,12 +626,12 @@ impl Executor { let feature_background = stream::iter(feature_background) .map(Ok) - .try_fold(None, |world, bg_step| { + .try_fold(before_hook, |world, bg_step| { self.run_step(world, bg_step, into_bg_step_ev).map_ok(Some) }) .await?; - let rule_background = rule + let rule_background = r .as_ref() .map(|rule| { rule.background @@ -473,36 +650,96 @@ impl Executor { }) .await?; - stream::iter(scenario.steps.iter().map(|s| Arc::new(s.clone()))) - .map(Ok) - .try_fold(rule_background, |world, step| { - self.run_step(world, step, into_step_ev).map_ok(Some) - }) - .await + let steps = + stream::iter(s.steps.iter().map(|s| Arc::new(s.clone()))) + .map(Ok) + .try_fold(rule_background, |world, step| { + self.run_step(world, step, into_step_ev).map_ok(Some) + }) + .await?; + + self.run_hook(Hook::After, steps, &f, r.as_ref(), &s).await }; drop(res.await); - self.send(event::Cucumber::scenario( - feature.clone(), - rule.clone(), - scenario.clone(), - event::Scenario::Finished, + self.send(Cucumber::scenario( + f.clone(), + r.clone(), + s.clone(), + Finished, )); - if let Some(rule) = rule { - if let Some(finished) = - self.rule_scenario_finished(feature.clone(), rule) - { - self.send(finished); + if let Some(rule) = r { + if let Some(fin) = self.rule_scenario_finished(f.clone(), rule) { + self.send(fin); } } - if let Some(finished) = self.feature_scenario_finished(feature) { - self.send(finished); + if let Some(fin) = self.feature_scenario_finished(f) { + self.send(fin); } } + /// TODO + async fn run_hook( + &self, + which: Hook, + world: Option, + feature: &Arc, + rule: Option<&Arc>, + scenario: &Arc, + ) -> Result, ()> { + let init_world = async { + Ok(if let Some(w) = world { + w + } else { + let world_fut = async { + W::new().await.expect("failed to initialize World") + }; + match AssertUnwindSafe(world_fut).catch_unwind().await { + Ok(world) => world, + Err(_info) => return Err(()), + } + }) + }; + + match which { + Hook::Before => { + if let Some(hook) = self.before_hook.as_ref() { + let mut world = init_world.await?; + let fut = (hook)( + feature.as_ref(), + rule.as_ref().map(AsRef::as_ref), + scenario.as_ref(), + &mut world, + ); + match AssertUnwindSafe(fut).catch_unwind().await { + Ok(()) => return Ok(Some(world)), + Err(_info) => return Err(()), + } + } + } + Hook::After => { + if let Some(hook) = self.after_hook.as_ref() { + let mut world = init_world.await?; + let fut = (hook)( + feature.as_ref(), + rule.as_ref().map(AsRef::as_ref), + scenario.as_ref(), + &mut world, + ); + match AssertUnwindSafe(fut).catch_unwind().await { + Ok(()) => return Ok(Some(world)), + Err(_info) => return Err(()), + } + } + } + }; + + Ok(None) + } + /// Runs a [`Step`]. /// /// # Events diff --git a/tests/wait.rs b/tests/wait.rs index c947d7a0..0b697dbb 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -3,11 +3,26 @@ use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; use cucumber::{given, then, when, WorldInit}; use futures::FutureExt as _; -use tokio::time; +use tokio::{time, time::sleep}; #[tokio::main] async fn main() { - let res = World::run("tests/features/wait"); + let res = World::cucumber() + .before(|_, _, _, w| { + async move { + w.0 = 0; + sleep(Duration::from_millis(10)).await; + } + .boxed_local() + }) + .after(|_, _, _, w| { + async move { + w.0 = 0; + sleep(Duration::from_millis(10)).await; + } + .boxed_local() + }) + .run_and_exit("tests/features/wait"); let err = AssertUnwindSafe(res) .catch_unwind() From 72ae55240f64c2b6f20642b40bd48d156cd486ef Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 15 Oct 2021 12:19:29 +0300 Subject: [PATCH 2/9] Implement hook events --- src/cucumber.rs | 28 +++- src/event.rs | 85 +++++++++- src/runner/basic.rs | 288 +++++++++++++++++++++++----------- src/writer/basic.rs | 39 ++++- src/writer/fail_on_skipped.rs | 4 + src/writer/mod.rs | 4 + src/writer/normalized.rs | 4 + src/writer/repeat.rs | 8 +- src/writer/summarized.rs | 87 ++++++++-- tests/wait.rs | 5 +- 10 files changed, 443 insertions(+), 109 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index c4e401d4..658ce34e 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -875,7 +875,12 @@ impl Cucumber, Wr> { } } - /// TODO + /// Sets hook, executed on every [`Scenario`] before any [`Step`]s, + /// including [`Background`] ones. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step #[must_use] pub fn before( self, @@ -904,7 +909,19 @@ impl Cucumber, Wr> { } } - /// TODO + /// Sets hook, executed on every [`Scenario`] after all [`Step`]s. + /// + /// Last `World` argument is supplied to the function, in case it + /// was initialized before by [`before`] hook or any non-failed [`Step`]. + /// In case last [`Scenario`]'s [`Step`] failed, we want to return event + /// with exact `World` state. Also we don't want to impose additional + /// [`Clone`] bounds on `World`, so the only option left is to pass [`None`] + /// to the function. + /// + /// + /// [`before`]: Self::before() + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step #[must_use] pub fn after( self, @@ -915,7 +932,7 @@ impl Cucumber, Wr> { &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, - &'a mut W, + Option<&'a mut W>, ) -> LocalBoxFuture<'a, ()>, { let Self { @@ -1074,12 +1091,15 @@ where if writer.execution_has_failed() { let failed_steps = writer.failed_steps(); let parsing_errors = writer.parsing_errors(); + let hook_errors = writer.hook_errors(); panic!( - "{} step{} failed, {} parsing error{}", + "{} step{} failed, {} parsing error{}, {} hook error{}", failed_steps, (failed_steps != 1).then(|| "s").unwrap_or_default(), parsing_errors, (parsing_errors != 1).then(|| "s").unwrap_or_default(), + hook_errors, + (hook_errors != 1).then(|| "s").unwrap_or_default(), ); } } diff --git a/src/event.rs b/src/event.rs index 4c32e0e0..22b80530 100644 --- a/src/event.rs +++ b/src/event.rs @@ -19,7 +19,7 @@ //! [`Runner`]: crate::Runner //! [Cucumber]: https://cucumber.io -use std::{any::Any, sync::Arc}; +use std::{any::Any, fmt, sync::Arc}; /// Alias for a [`catch_unwind()`] error. /// @@ -224,6 +224,63 @@ impl Clone for Step { } } +/// Type of the hook, executed before or after all [`Scenario`]'s [`Step`]s. +/// +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +#[derive(Clone, Copy, Debug)] +pub enum HookTy { + /// Hook, executed on every [`Scenario`] before any [`Step`]s. + /// + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step + Before, + + /// Hook, executed on every [`Scenario`] after all [`Step`]s. + /// + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step + After, +} + +/// [`Before`] or [`After`] hook event. +/// +/// [`After`]: HookTy::After +/// [`Before`]: HookTy::Before +#[derive(Debug)] +pub enum Hook { + /// Hook execution being started. + Started, + + /// Hook passed. + Passed, + + /// Hook failed. + Failed(Option>, Info), +} + +impl fmt::Display for HookTy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + HookTy::Before => "Before", + HookTy::After => "After", + }; + write!(f, "{}", s) + } +} + +// Manual implementation is required to omit the redundant `World: Clone` trait +// bound imposed by `#[derive(Clone)]`. +impl Clone for Hook { + fn clone(&self) -> Self { + match self { + Hook::Started => Hook::Started, + Hook::Passed => Hook::Passed, + Hook::Failed(w, i) => Hook::Failed(w.clone(), i.clone()), + } + } +} + /// Event specific to a particular [Scenario]. /// /// [Scenario]: https://cucumber.io/docs/gherkin/reference/#example @@ -234,6 +291,9 @@ pub enum Scenario { /// [`Scenario`]: gherkin::Scenario Started, + /// [`Hook`] event. + Hook(HookTy, Hook), + /// [`Background`] [`Step`] event. /// /// [`Background`]: gherkin::Background @@ -254,6 +314,7 @@ impl Clone for Scenario { fn clone(&self) -> Self { match self { Self::Started => Self::Started, + Self::Hook(ty, ev) => Self::Hook(*ty, ev.clone()), Self::Background(bg, ev) => { Self::Background(bg.clone(), ev.clone()) } @@ -264,6 +325,28 @@ impl Clone for Scenario { } impl Scenario { + /// Constructs an event of a hook being started. + #[must_use] + pub fn hook_started(which: HookTy) -> Self { + Self::Hook(which, Hook::Started) + } + + /// Constructs an event of a passed hook. + #[must_use] + pub fn hook_passed(which: HookTy) -> Self { + Self::Hook(which, Hook::Passed) + } + + /// Constructs an event of a failed hook. + #[must_use] + pub fn hook_failed( + which: HookTy, + world: Option>, + info: Info, + ) -> Self { + Self::Hook(which, Hook::Failed(world, info)) + } + /// Constructs an event of a [`Step`] being started. /// /// [`Step`]: gherkin::Step diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 4ac4b11e..6a372d17 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -35,7 +35,7 @@ use itertools::Itertools as _; use regex::{CaptureLocations, Regex}; use crate::{ - event::{self, Info}, + event::{self, HookTy, Info}, feature::Ext as _, parser, step, Runner, Step, World, }; @@ -69,8 +69,8 @@ pub enum ScenarioType { pub struct Basic< World, F = WhichScenarioFn, - Before = HookFn, - After = HookFn, + Before = BeforeHookFn, + After = AfterHookFn, > { /// Optional number of concurrently executed [`Scenario`]s. /// @@ -90,10 +90,19 @@ pub struct Basic< /// [`Scenario`]: gherkin::Scenario which_scenario: F, - /// TODO + /// Function, executed on every [`Scenario`] before any [`Step`]s, + /// including [`Background`] ones. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step before_hook: Option, - /// TODO + /// Function, executed on every [`Scenario`] after all [`Step`]s. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step after_hook: Option, } @@ -109,22 +118,31 @@ pub type WhichScenarioFn = fn( &gherkin::Scenario, ) -> ScenarioType; -/// TODO -pub type HookFn = for<'a> fn( +/// Alias for [`fn`] executed on every [`Scenario`] before any [`Step`]s. +/// +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +pub type BeforeHookFn = for<'a> fn( &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, &'a mut World, ) -> LocalBoxFuture<'a, ()>; -enum Hook { - Before, - After, -} +/// Alias for [`fn`] executed on every [`Scenario`] after all [`Step`]s. +/// +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +pub type AfterHookFn = for<'a> fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + Option<&'a mut World>, +) -> LocalBoxFuture<'a, ()>; // Implemented manually to omit redundant trait bounds on `World` and to omit // outputting `F`. -impl fmt::Debug for Basic { +impl fmt::Debug for Basic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Basic") .field("max_concurrent_scenarios", &self.max_concurrent_scenarios) @@ -214,7 +232,12 @@ impl Basic { } } - /// TODO + /// Sets hook, executed on every [`Scenario`] before any [`Step`]s, + /// including [`Background`] ones. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step #[must_use] pub fn before(self, func: Func) -> Basic where @@ -241,7 +264,19 @@ impl Basic { } } - /// TODO + /// Sets hook, executed on every [`Scenario`] after all [`Step`]s. + /// + /// Last `World` argument is supplied to the function, in case it + /// was initialized before by [`before`] hook or any non-failed [`Step`]. + /// In case last [`Scenario`]'s [`Step`] failed, we want to return event + /// with exact `World` state. Also we don't want to impose additional + /// [`Clone`] bounds on `World`, so the only option left is to pass [`None`] + /// to the function. + /// + /// + /// [`before`]: Self::before() + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step #[must_use] pub fn after(self, func: Func) -> Basic where @@ -249,7 +284,7 @@ impl Basic { &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, - &'a mut World, + Option<&'a mut World>, ) -> LocalBoxFuture<'a, ()>, { let Self { @@ -314,20 +349,20 @@ where &gherkin::Scenario, ) -> ScenarioType + 'static, - Before: 'static - + for<'a> Fn( + Before: for<'a> Fn( &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, &'a mut W, - ) -> LocalBoxFuture<'a, ()>, - After: 'static - + for<'a> Fn( + ) -> LocalBoxFuture<'a, ()> + + 'static, + After: for<'a> Fn( &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, - &'a mut W, - ) -> LocalBoxFuture<'a, ()>, + Option<&'a mut W>, + ) -> LocalBoxFuture<'a, ()> + + 'static, { type EventStream = LocalBoxStream<'static, parser::Result>>; @@ -430,7 +465,7 @@ async fn execute( &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, - &'a mut W, + Option<&'a mut W>, ) -> LocalBoxFuture<'a, ()>, { // Those panic hook shenanigans are done to avoid console messages like @@ -504,10 +539,19 @@ struct Executor { /// [`Step`]: step::Step collection: step::Collection, - /// TODO + /// Function, executed on every [`Scenario`] before any [`Step`]s, + /// including [`Background`] ones. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step before_hook: Option, - /// TODO + /// Function, executed on every [`Scenario`] after all [`Step`]s. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step after_hook: Option, /// Sender for notifying state of [`Feature`]s completion. @@ -530,7 +574,7 @@ where &'a gherkin::Feature, Option<&'a gherkin::Rule>, &'a gherkin::Scenario, - &'a mut W, + Option<&'a mut W>, ) -> LocalBoxFuture<'a, ()>, { /// Creates a new [`Executor`]. @@ -575,15 +619,13 @@ where let ok = |e: fn(Arc) -> event::Scenario| { let (f, r, s) = (&f, &r, &s); move |step| { - let (f, r, s) = (f.clone(), r.clone(), s.clone()); - Cucumber::scenario(f, r, s, e(step)) + Cucumber::scenario(f.clone(), r.clone(), s.clone(), e(step)) } }; let ok_capt = |e: fn(Arc, _) -> event::Scenario| { let (f, r, s) = (&f, &r, &s); - move |step, captures| { - let (f, r, s) = (f.clone(), r.clone(), s.clone()); - Cucumber::scenario(f, r, s, e(step, captures)) + move |st, cap| { + Cucumber::scenario(f.clone(), r.clone(), s.clone(), e(st, cap)) } }; let err = |e: fn(Arc, _, _, _) -> event::Scenario| { @@ -614,8 +656,9 @@ where let res = async { let before_hook = self - .run_hook(Hook::Before, None, &f, r.as_ref(), &s) - .await?; + .run_before_hook(&f, r.as_ref(), &s) + .await + .map_err(|_| None)?; let feature_background = f .background @@ -650,18 +693,19 @@ where }) .await?; - let steps = - stream::iter(s.steps.iter().map(|s| Arc::new(s.clone()))) - .map(Ok) - .try_fold(rule_background, |world, step| { - self.run_step(world, step, into_step_ev).map_ok(Some) - }) - .await?; + stream::iter(s.steps.iter().map(|s| Arc::new(s.clone()))) + .map(Ok) + .try_fold(rule_background, |world, step| { + self.run_step(world, step, into_step_ev).map_ok(Some) + }) + .await + }; - self.run_hook(Hook::After, steps, &f, r.as_ref(), &s).await + let world = match res.await { + Ok(world) | Err(world) => world, }; - drop(res.await); + drop(self.run_after_hook(world, &f, r.as_ref(), &s).await); self.send(Cucumber::scenario( f.clone(), @@ -681,61 +725,127 @@ where } } - /// TODO - async fn run_hook( + /// Executes [`HookTy::Before`], if present. + async fn run_before_hook( &self, - which: Hook, - world: Option, feature: &Arc, rule: Option<&Arc>, scenario: &Arc, ) -> Result, ()> { let init_world = async { - Ok(if let Some(w) = world { - w - } else { - let world_fut = async { - W::new().await.expect("failed to initialize World") + let world_fut = + async { W::new().await.expect("failed to initialize World") }; + + AssertUnwindSafe(world_fut) + .catch_unwind() + .await + .map_err(|info| (info, None)) + }; + + if let Some(hook) = self.before_hook.as_ref() { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.map(Arc::clone), + scenario.clone(), + event::Scenario::hook_started(HookTy::Before), + )); + + let fut = init_world.and_then(|mut world| async { + let fut = (hook)( + feature.as_ref(), + rule.as_ref().map(AsRef::as_ref), + scenario.as_ref(), + &mut world, + ); + return match AssertUnwindSafe(fut).catch_unwind().await { + Ok(()) => Ok(world), + Err(info) => Err((info, Some(world))), }; - match AssertUnwindSafe(world_fut).catch_unwind().await { - Ok(world) => world, - Err(_info) => return Err(()), + }); + + return match fut.await { + Ok(world) => { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.map(Arc::clone), + scenario.clone(), + event::Scenario::hook_passed(HookTy::Before), + )); + Ok(Some(world)) } - }) - }; + Err((info, world)) => { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.map(Arc::clone), + scenario.clone(), + event::Scenario::hook_failed( + HookTy::Before, + world.map(Arc::new), + info.into(), + ), + )); + Err(()) + } + }; + } + + Ok(None) + } + + /// Executes [`HookTy::After`], if present. + async fn run_after_hook( + &self, + mut world: Option, + feature: &Arc, + rule: Option<&Arc>, + scenario: &Arc, + ) -> Result, ()> { + if let Some(hook) = self.after_hook.as_ref() { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.map(Arc::clone), + scenario.clone(), + event::Scenario::hook_started(HookTy::After), + )); + + let fut = async { + let fut = (hook)( + feature.as_ref(), + rule.as_ref().map(AsRef::as_ref), + scenario.as_ref(), + world.as_mut(), + ); + return match AssertUnwindSafe(fut).catch_unwind().await { + Ok(()) => Ok(world), + Err(info) => Err((info, world)), + }; + }; - match which { - Hook::Before => { - if let Some(hook) = self.before_hook.as_ref() { - let mut world = init_world.await?; - let fut = (hook)( - feature.as_ref(), - rule.as_ref().map(AsRef::as_ref), - scenario.as_ref(), - &mut world, - ); - match AssertUnwindSafe(fut).catch_unwind().await { - Ok(()) => return Ok(Some(world)), - Err(_info) => return Err(()), - } + return match fut.await { + Ok(world) => { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.map(Arc::clone), + scenario.clone(), + event::Scenario::hook_passed(HookTy::After), + )); + Ok(world) } - } - Hook::After => { - if let Some(hook) = self.after_hook.as_ref() { - let mut world = init_world.await?; - let fut = (hook)( - feature.as_ref(), - rule.as_ref().map(AsRef::as_ref), - scenario.as_ref(), - &mut world, - ); - match AssertUnwindSafe(fut).catch_unwind().await { - Ok(()) => return Ok(Some(world)), - Err(_info) => return Err(()), - } + Err((info, world)) => { + self.send(event::Cucumber::scenario( + feature.clone(), + rule.map(Arc::clone), + scenario.clone(), + event::Scenario::hook_failed( + HookTy::After, + world.map(Arc::new), + info.into(), + ), + )); + Err(()) } - } - }; + }; + } Ok(None) } @@ -752,7 +862,7 @@ where mut world: Option, step: Arc, (started, passed, skipped, failed): (St, Ps, Sk, F), - ) -> Result + ) -> Result> where St: FnOnce(Arc) -> event::Cucumber, Ps: FnOnce(Arc, CaptureLocations) -> event::Cucumber, @@ -798,7 +908,7 @@ where } Ok(None) => { self.send(skipped(step)); - Err(()) + Err(world) } Err((err, captures)) => { self.send(failed( @@ -807,7 +917,7 @@ where world.map(Arc::new), Arc::from(err), )); - Err(()) + Err(None) } } } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 1fd52150..8735e4e6 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -193,12 +193,22 @@ impl Basic { scenario: &gherkin::Scenario, ev: &event::Scenario, ) { - use event::Scenario; + use event::{Hook, Scenario}; match ev { Scenario::Started => { self.scenario_started(scenario); } + Scenario::Hook(_, Hook::Started) => { + self.indent += 4; + } + Scenario::Hook(which, Hook::Failed(world, info)) => { + self.hook_failed(feat, scenario, *which, world.as_ref(), info); + self.indent = self.indent.saturating_sub(4); + } + Scenario::Hook(_, Hook::Passed) => { + self.indent = self.indent.saturating_sub(4); + } Scenario::Background(bg, ev) => { self.background(feat, bg, ev); } @@ -209,6 +219,33 @@ impl Basic { } } + fn hook_failed( + &mut self, + feat: &gherkin::Feature, + sc: &gherkin::Scenario, + which: event::HookTy, + world: Option<&W>, + info: &Info, + ) { + self.clear_last_lines_if_term_present(); + + self.write_line(&self.styles.err(format!( + "{indent}\u{2718} {} hook failed {}:{}:{}\n\ + {indent} Captured output: {}{}", + which, + feat.path + .as_ref() + .and_then(|p| p.to_str()) + .unwrap_or(&feat.name), + sc.position.line, + sc.position.col, + coerce_error(info), + format_world(world, self.indent.saturating_sub(3) + 3), + indent = " ".repeat(self.indent.saturating_sub(3)), + ))) + .unwrap(); + } + /// Outputs [started] [`Scenario`] to STDOUT. /// /// [started]: event::Scenario::Started diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index 6bbcf402..3cbae312 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -120,6 +120,10 @@ where fn parsing_errors(&self) -> usize { self.writer.parsing_errors() } + + fn hook_errors(&self) -> usize { + self.writer.hook_errors() + } } impl From for FailOnSkipped { diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 4626506a..02bc176d 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -83,6 +83,10 @@ pub trait Failure: Writer { /// Returns number of parsing errors. #[must_use] fn parsing_errors(&self) -> usize; + + /// Returns number of failed hooks. + #[must_use] + fn hook_errors(&self) -> usize; } /// Extension of [`Writer`] allowing its normalization and summarization. diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 1a403245..a7434281 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -134,6 +134,10 @@ where fn parsing_errors(&self) -> usize { self.writer.parsing_errors() } + + fn hook_errors(&self) -> usize { + self.writer.hook_errors() + } } /// Normalization queue for incoming events. diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 15e8d850..23b4916a 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -94,6 +94,10 @@ where fn parsing_errors(&self) -> usize { self.writer.parsing_errors() } + + fn hook_errors(&self) -> usize { + self.writer.hook_errors() + } } impl Repeat { @@ -151,7 +155,7 @@ impl Repeat { /// [`Parser`]: crate::Parser #[must_use] pub fn failed(writer: Wr) -> Self { - use event::{Cucumber, Feature, Rule, Scenario, Step}; + use event::{Cucumber, Feature, Hook, Rule, Scenario, Step}; Self { writer, @@ -166,11 +170,13 @@ impl Repeat { _, Scenario::Step(_, Step::Failed(..)) | Scenario::Background(_, Step::Failed(..)) + | Scenario::Hook(_, Hook::Failed(..)) ) ) | Feature::Scenario( _, Scenario::Step(_, Step::Failed(..)) | Scenario::Background(_, Step::Failed(..)) + | Scenario::Hook(_, Hook::Failed(..)) ) )) | Err(_) ) diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index 6f383610..9f1621e7 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -10,7 +10,7 @@ //! [`Writer`]-wrapper for collecting a summary of execution. -use std::{array, borrow::Cow, collections::HashSet, sync::Arc}; +use std::{array, borrow::Cow, collections::HashMap, sync::Arc}; use async_trait::async_trait; use derive_more::Deref; @@ -96,10 +96,13 @@ pub struct Summarized { /// [`Parser`]: crate::Parser pub parsing_errors: usize, + /// Number of failed hooks. + pub failed_hooks: usize, + /// Handled [`Scenario`]s to collect [`Stats`]. /// /// [`Scenario`]: gherkin::Scenario - handled_scenarios: HashSet>, + handled_scenarios: HashMap, FailedOrSkipped>, } /// Alias for [`fn`] used to determine should [`Skipped`] test considered as @@ -110,6 +113,26 @@ pub struct Summarized { pub type SkipFn = fn(&gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario) -> bool; +/// [`Failed`] or [`Skipped`] [`Scenario`]s. +/// +/// [`Failed`]: event::Step::Failed +/// [`Scenario`]: gherkin::Scenario +/// [`Skipped`]: event::Step::Skipped +#[derive(Clone, Copy, Debug)] +enum FailedOrSkipped { + /// [`Failed`] [`Scenario`]. + /// + /// [`Failed`]: event::Step::Failed + /// [`Scenario`]: gherkin::Scenario + Failed, + + /// [`Skipped`] [`Scenario`]. + /// + /// [`Scenario`]: gherkin::Scenario + /// [`Skipped`]: event::Step::Skipped + Skipped, +} + #[async_trait(?Send)] impl Writer for Summarized where @@ -173,6 +196,10 @@ where fn parsing_errors(&self) -> usize { self.parsing_errors } + + fn hook_errors(&self) -> usize { + self.failed_hooks + } } impl From for Summarized { @@ -192,7 +219,8 @@ impl From for Summarized { failed: 0, }, parsing_errors: 0, - handled_scenarios: HashSet::new(), + failed_hooks: 0, + handled_scenarios: HashMap::new(), } } } @@ -206,7 +234,10 @@ impl Summarized { scenario: &Arc, ev: &event::Step, ) { - use event::Step; + use self::{ + event::Step, + FailedOrSkipped::{Failed, Skipped}, + }; match ev { Step::Started => {} @@ -214,12 +245,13 @@ impl Summarized { Step::Skipped => { self.steps.skipped += 1; self.scenarios.skipped += 1; - let _ = self.handled_scenarios.insert(scenario.clone()); + let _ = + self.handled_scenarios.insert(scenario.clone(), Skipped); } Step::Failed(..) => { self.steps.failed += 1; self.scenarios.failed += 1; - let _ = self.handled_scenarios.insert(scenario.clone()); + let _ = self.handled_scenarios.insert(scenario.clone(), Failed); } } } @@ -232,15 +264,38 @@ impl Summarized { scenario: &Arc, ev: &event::Scenario, ) { - use event::Scenario; + use event::{Hook, Scenario}; match ev { - Scenario::Started => {} + Scenario::Started + | Scenario::Hook(_, Hook::Passed | Hook::Started) => {} + Scenario::Hook(_, Hook::Failed(..)) => { + // - If Scenario's last Step failed and then After Hook failed + // too, we don't need to track second failure; + // - If Scenario's last Step was skipped and then After Hook + // failed, we need to override skipped Scenario with failed; + // - If Scenario executed no Steps and then Hook failed, we + // track Scenario as failed. + match self.handled_scenarios.get(scenario) { + Some(FailedOrSkipped::Failed) => {} + Some(FailedOrSkipped::Skipped) => { + self.scenarios.skipped -= 1; + self.scenarios.failed += 1; + } + None => { + self.scenarios.failed += 1; + let _ = self + .handled_scenarios + .insert(scenario.clone(), FailedOrSkipped::Failed); + } + } + self.failed_hooks += 1; + } Scenario::Background(_, ev) | Scenario::Step(_, ev) => { self.handle_step(scenario, ev); } Scenario::Finished => { - if !self.handled_scenarios.remove(scenario) { + if self.handled_scenarios.remove(scenario).is_none() { self.scenarios.passed += 1; } } @@ -281,8 +336,18 @@ impl Styles { }) .unwrap_or_default(); + let hook_errors = (summary.failed_hooks > 0) + .then(|| { + self.err(self.maybe_plural("hook error", summary.failed_hooks)) + }) + .unwrap_or_default(); + + let comma = (!parsing_errors.is_empty() && !hook_errors.is_empty()) + .then(|| self.err(",")) + .unwrap_or_default(); + format!( - "{}\n{}\n{}{}{}\n{}{}\n{}", + "{}\n{}\n{}{}{}\n{}{}\n{}{} {}", self.bold(self.header("[Summary]")), features, rules, @@ -291,6 +356,8 @@ impl Styles { steps, steps_stats, parsing_errors, + comma, + hook_errors ) .trim_end_matches('\n') .to_string() diff --git a/tests/wait.rs b/tests/wait.rs index 0b697dbb..4a505cfb 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -15,9 +15,8 @@ async fn main() { } .boxed_local() }) - .after(|_, _, _, w| { + .after(|_, _, _, _| { async move { - w.0 = 0; sleep(Duration::from_millis(10)).await; } .boxed_local() @@ -30,7 +29,7 @@ async fn main() { .expect_err("should err"); let err = err.downcast_ref::().unwrap(); - assert_eq!(err, "2 steps failed, 1 parsing error"); + assert_eq!(err, "2 steps failed, 1 parsing error, 0 hook errors"); } #[given(regex = r"(\d+) secs?")] From b740341238885bc52b4551e0af19a3fbb942e6cd Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 15 Oct 2021 13:03:51 +0300 Subject: [PATCH 3/9] Mention hooks in Features book chapter --- book/src/Features.md | 76 ++++++++++++++++++++++++++++++++++++++++++++ tests/wait.rs | 6 ++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/book/src/Features.md b/book/src/Features.md index 8f6f5c9e..3b221dfe 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -41,6 +41,8 @@ Occasionally you’ll find yourself repeating the same `Given` steps in all the Since it's repeated in every scenario, this is an indication that those steps are not essential to describe the scenarios, so they are _incidental details_. You can literally move such `Given` steps to background, by grouping them under a `Background` section. +A `Background` allows you to add some context to the scenarios that follow it. It can contain one or more steps, which are run before each scenario, but after any [`Before` hooks](#before-hook). + ```gherkin Feature: Animal feature @@ -304,5 +306,79 @@ In case most of your `.feature` files aren't written in English and you want to +## Scenario hooks + +### Before hook + +`Before` hooks run before the first step of each scenario, even [Background](#background-keyword) ones. + +```rust +# use std::{convert::Infallible, time::Duration}; +# +# use async_trait::async_trait; +# use cucumber::WorldInit; +# use futures::FutureExt as _; +# use tokio::time::sleep; +# +# #[derive(Debug, WorldInit)] +# struct World; +# +# #[async_trait(?Send)] +# impl cucumber::World for World { +# type Error = Infallible; +# +# async fn new() -> Result { +# Ok(World) +# } +# } +# +# fn main() { +World::cucumber() + .before(|_feature, _rule, _scenario, _world| { + sleep(Duration::from_millis(10)).boxed_local() + }) + .run_and_exit("tests/features/book"); +# } +``` + +> #### Think twice before you use `Before` +> Whatever happens in a `Before` hook is invisible to people who only read the features. You should consider using a [Background](#background-keyword) as a more explicit alternative, especially if the setup should be readable by non-technical people. Only use a `Before` hook for low-level logic such as starting a browser or deleting data from a database. + +### After hook + +`After` hooks run after the last step of each scenario, even when the step result is `failed` or `skipped`. + +```rust +# use std::{convert::Infallible, time::Duration}; +# +# use async_trait::async_trait; +# use cucumber::WorldInit; +# use futures::FutureExt as _; +# use tokio::time::sleep; +# +# #[derive(Debug, WorldInit)] +# struct World; +# +# #[async_trait(?Send)] +# impl cucumber::World for World { +# type Error = Infallible; +# +# async fn new() -> Result { +# Ok(World) +# } +# } +# +# fn main() { +World::cucumber() + .after(|_feature, _rule, _scenario, _world| { + sleep(Duration::from_millis(10)).boxed_local() + }) + .run_and_exit("tests/features/book"); +# } +``` + + + + [Cucumber]: https://cucumber.io [Gherkin]: https://cucumber.io/docs/gherkin diff --git a/tests/wait.rs b/tests/wait.rs index 4a505cfb..113d9271 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -3,7 +3,7 @@ use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; use cucumber::{given, then, when, WorldInit}; use futures::FutureExt as _; -use tokio::{time, time::sleep}; +use tokio::time; #[tokio::main] async fn main() { @@ -11,13 +11,13 @@ async fn main() { .before(|_, _, _, w| { async move { w.0 = 0; - sleep(Duration::from_millis(10)).await; + time::sleep(Duration::from_millis(10)).await; } .boxed_local() }) .after(|_, _, _, _| { async move { - sleep(Duration::from_millis(10)).await; + time::sleep(Duration::from_millis(10)).await; } .boxed_local() }) From c49280b4ce107f61130fda8356dac3fa7bd1b01e Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 15 Oct 2021 13:41:11 +0300 Subject: [PATCH 4/9] Corrections --- src/cucumber.rs | 5 ++++- src/event.rs | 12 +++++++++--- src/runner/basic.rs | 5 ++++- src/writer/basic.rs | 2 +- src/writer/mod.rs | 4 +++- src/writer/summarized.rs | 4 +++- tests/wait.rs | 5 +---- 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index 658ce34e..06d26ee7 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -909,7 +909,8 @@ impl Cucumber, Wr> { } } - /// Sets hook, executed on every [`Scenario`] after all [`Step`]s. + /// Sets hook, executed on every [`Scenario`] after all [`Step`]s even after + /// [`Skipped`] of [`Failed`] [`Step`]s. /// /// Last `World` argument is supplied to the function, in case it /// was initialized before by [`before`] hook or any non-failed [`Step`]. @@ -920,7 +921,9 @@ impl Cucumber, Wr> { /// /// /// [`before`]: Self::before() + /// [`Failed`]: event::Step::Failed /// [`Scenario`]: gherkin::Scenario + /// [`Skipped`]: event::Step::Skipped /// [`Step`]: gherkin::Step #[must_use] pub fn after( diff --git a/src/event.rs b/src/event.rs index 22b80530..40717e36 100644 --- a/src/event.rs +++ b/src/event.rs @@ -325,19 +325,25 @@ impl Clone for Scenario { } impl Scenario { - /// Constructs an event of a hook being started. + /// Constructs an event of a [`Scenario`] hook being started. + /// + /// [`Scenario`]: gherkin::Scenario #[must_use] pub fn hook_started(which: HookTy) -> Self { Self::Hook(which, Hook::Started) } - /// Constructs an event of a passed hook. + /// Constructs an event of a passed [`Scenario`] hook. + /// + /// [`Scenario`]: gherkin::Scenario #[must_use] pub fn hook_passed(which: HookTy) -> Self { Self::Hook(which, Hook::Passed) } - /// Constructs an event of a failed hook. + /// Constructs an event of a failed [`Scenario`] hook. + /// + /// [`Scenario`]: gherkin::Scenario #[must_use] pub fn hook_failed( which: HookTy, diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 6a372d17..a9dc67d0 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -264,7 +264,8 @@ impl Basic { } } - /// Sets hook, executed on every [`Scenario`] after all [`Step`]s. + /// Sets hook, executed on every [`Scenario`] after all [`Step`]s even after + /// [`Skipped`] of [`Failed`] [`Step`]s. /// /// Last `World` argument is supplied to the function, in case it /// was initialized before by [`before`] hook or any non-failed [`Step`]. @@ -275,7 +276,9 @@ impl Basic { /// /// /// [`before`]: Self::before() + /// [`Failed`]: event::Step::Failed /// [`Scenario`]: gherkin::Scenario + /// [`Skipped`]: event::Step::Skipped /// [`Step`]: gherkin::Step #[must_use] pub fn after(self, func: Func) -> Basic diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 8735e4e6..b44230ca 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -230,7 +230,7 @@ impl Basic { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.err(format!( - "{indent}\u{2718} {} hook failed {}:{}:{}\n\ + "{indent}\u{2718} {} Scenario hook failed {}:{}:{}\n\ {indent} Captured output: {}{}", which, feat.path diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 02bc176d..1b708cef 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -84,7 +84,9 @@ pub trait Failure: Writer { #[must_use] fn parsing_errors(&self) -> usize; - /// Returns number of failed hooks. + /// Returns number of failed [`Scenario`] hooks. + /// + /// [`Scenario`]: gherkin::Scenario #[must_use] fn hook_errors(&self) -> usize; } diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index 9f1621e7..77da0149 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -96,7 +96,9 @@ pub struct Summarized { /// [`Parser`]: crate::Parser pub parsing_errors: usize, - /// Number of failed hooks. + /// Number of failed [`Scenario`] hooks. + /// + /// [`Scenario`]: gherkin::Scenario pub failed_hooks: usize, /// Handled [`Scenario`]s to collect [`Stats`]. diff --git a/tests/wait.rs b/tests/wait.rs index 113d9271..08c6a79c 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -16,10 +16,7 @@ async fn main() { .boxed_local() }) .after(|_, _, _, _| { - async move { - time::sleep(Duration::from_millis(10)).await; - } - .boxed_local() + time::sleep(Duration::from_millis(10)).boxed_local() }) .run_and_exit("tests/features/wait"); From 192bace492b87e1aba96e6a901940afbedf15030 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 15 Oct 2021 14:49:25 +0300 Subject: [PATCH 5/9] CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c50d4c..e4f4313b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - Made test callbacks first argument `&mut World` instead of `World`. ([#128]) - Made `#[step]` argument of step functions `Step` instead of `StepContext` again, while test callbacks still receive `StepContext` as a second parameter. ([#128]) - Deprecated `--nocapture` and `--debug` CLI options to be completely redesigned in `0.11` release. ([#137]) -- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) were removed, but are planned to be re-implemented with some changes in `0.11` release. ([#128]) +- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept `&mut World` as a last parameter ([#142]) ### Added @@ -32,6 +32,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [#128]: /../../pull/128 [#136]: /../../pull/136 [#137]: /../../pull/137 +[#142]: /../../pull/142 From 2d16c86b345b02914c84bc184e9eb5ad8f5c6e71 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 18 Oct 2021 13:51:42 +0300 Subject: [PATCH 6/9] Corrections --- CHANGELOG.md | 2 +- book/src/Features.md | 24 +++-- src/cucumber.rs | 56 ++++++---- src/event.rs | 44 ++++---- src/runner/basic.rs | 227 ++++++++++++++++++++------------------- src/writer/basic.rs | 45 ++++---- src/writer/summarized.rs | 70 ++++++------ tests/wait.rs | 2 +- 8 files changed, 246 insertions(+), 224 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f4313b..6e2506db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - Made test callbacks first argument `&mut World` instead of `World`. ([#128]) - Made `#[step]` argument of step functions `Step` instead of `StepContext` again, while test callbacks still receive `StepContext` as a second parameter. ([#128]) - Deprecated `--nocapture` and `--debug` CLI options to be completely redesigned in `0.11` release. ([#137]) -- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept `&mut World` as a last parameter ([#142]) +- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept optional `&mut World` as their last parameter. ([#142]) ### Added diff --git a/book/src/Features.md b/book/src/Features.md index 3b221dfe..84092ce2 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -41,7 +41,7 @@ Occasionally you’ll find yourself repeating the same `Given` steps in all the Since it's repeated in every scenario, this is an indication that those steps are not essential to describe the scenarios, so they are _incidental details_. You can literally move such `Given` steps to background, by grouping them under a `Background` section. -A `Background` allows you to add some context to the scenarios that follow it. It can contain one or more steps, which are run before each scenario, but after any [`Before` hooks](#before-hook). +`Background` allows you to add some context to the `Scenario`s following it. It can contain one or more steps, which are run before each scenario (but after any [`Before` hooks](#before-hook)). ```gherkin Feature: Animal feature @@ -308,9 +308,10 @@ In case most of your `.feature` files aren't written in English and you want to ## Scenario hooks -### Before hook -`Before` hooks run before the first step of each scenario, even [Background](#background-keyword) ones. +### `Before` hook + +`Before` hook runs before the first step of each scenario, even before [`Background` ones](#background-keyword). ```rust # use std::{convert::Infallible, time::Duration}; @@ -318,7 +319,7 @@ In case most of your `.feature` files aren't written in English and you want to # use async_trait::async_trait; # use cucumber::WorldInit; # use futures::FutureExt as _; -# use tokio::time::sleep; +# use tokio::time; # # #[derive(Debug, WorldInit)] # struct World; @@ -335,18 +336,19 @@ In case most of your `.feature` files aren't written in English and you want to # fn main() { World::cucumber() .before(|_feature, _rule, _scenario, _world| { - sleep(Duration::from_millis(10)).boxed_local() + time::sleep(Duration::from_millis(10)).boxed_local() }) .run_and_exit("tests/features/book"); # } ``` -> #### Think twice before you use `Before` -> Whatever happens in a `Before` hook is invisible to people who only read the features. You should consider using a [Background](#background-keyword) as a more explicit alternative, especially if the setup should be readable by non-technical people. Only use a `Before` hook for low-level logic such as starting a browser or deleting data from a database. +> ⚠️ __Think twice before using `Before` hook!__ +> Whatever happens in a `Before` hook is invisible to people reading `.feature`s. You should consider using a [`Background`](#background-keyword) as a more explicit alternative, especially if the setup should be readable by non-technical people. Only use a `Before` hook for low-level logic such as starting a browser or deleting data from a database. -### After hook -`After` hooks run after the last step of each scenario, even when the step result is `failed` or `skipped`. +### `After` hook + +`After` hook runs after the last step of each `Scenario`, even when that step fails or is skipped. ```rust # use std::{convert::Infallible, time::Duration}; @@ -354,7 +356,7 @@ World::cucumber() # use async_trait::async_trait; # use cucumber::WorldInit; # use futures::FutureExt as _; -# use tokio::time::sleep; +# use tokio::time; # # #[derive(Debug, WorldInit)] # struct World; @@ -371,7 +373,7 @@ World::cucumber() # fn main() { World::cucumber() .after(|_feature, _rule, _scenario, _world| { - sleep(Duration::from_millis(10)).boxed_local() + time::sleep(Duration::from_millis(10)).boxed_local() }) .run_and_exit("tests/features/book"); # } diff --git a/src/cucumber.rs b/src/cucumber.rs index 06d26ee7..0087c0e1 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -875,8 +875,8 @@ impl Cucumber, Wr> { } } - /// Sets hook, executed on every [`Scenario`] before any [`Step`]s, - /// including [`Background`] ones. + /// Sets a hook, executed on each [`Scenario`] before running all its + /// [`Step`]s, including [`Background`] ones. /// /// [`Background`]: gherkin::Background /// [`Scenario`]: gherkin::Scenario @@ -909,15 +909,15 @@ impl Cucumber, Wr> { } } - /// Sets hook, executed on every [`Scenario`] after all [`Step`]s even after - /// [`Skipped`] of [`Failed`] [`Step`]s. + /// Sets a hook, executed on each [`Scenario`] after running all its + /// [`Step`]s, even after [`Skipped`] of [`Failed`] [`Step`]s. /// - /// Last `World` argument is supplied to the function, in case it - /// was initialized before by [`before`] hook or any non-failed [`Step`]. - /// In case last [`Scenario`]'s [`Step`] failed, we want to return event - /// with exact `World` state. Also we don't want to impose additional - /// [`Clone`] bounds on `World`, so the only option left is to pass [`None`] - /// to the function. + /// Last `World` argument is supplied to the function, in case it was + /// initialized before by running [`before`] hook or any non-failed + /// [`Step`]. In case the last [`Scenario`]'s [`Step`] failed, we want to + /// return event with an exact `World` state. Also, we don't want to impose + /// additional [`Clone`] bounds on `World`, so the only option left is to + /// pass [`None`] to the function. /// /// /// [`before`]: Self::before() @@ -1092,18 +1092,36 @@ where { let writer = self.filter_run(input, filter).await; if writer.execution_has_failed() { + let mut msg = Vec::with_capacity(3); + let failed_steps = writer.failed_steps(); + if failed_steps > 0 { + msg.push(format!( + "{} step{} failed", + failed_steps, + (failed_steps > 1).then(|| "s").unwrap_or_default(), + )); + } + let parsing_errors = writer.parsing_errors(); + if parsing_errors > 0 { + msg.push(format!( + "{} parsing error{}", + parsing_errors, + (parsing_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + let hook_errors = writer.hook_errors(); - panic!( - "{} step{} failed, {} parsing error{}, {} hook error{}", - failed_steps, - (failed_steps != 1).then(|| "s").unwrap_or_default(), - parsing_errors, - (parsing_errors != 1).then(|| "s").unwrap_or_default(), - hook_errors, - (hook_errors != 1).then(|| "s").unwrap_or_default(), - ); + if hook_errors > 0 { + msg.push(format!( + "{} hook error{}", + hook_errors, + (hook_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + panic!("{}", msg.join(", ")); } } } diff --git a/src/event.rs b/src/event.rs index 40717e36..23d57439 100644 --- a/src/event.rs +++ b/src/event.rs @@ -224,29 +224,35 @@ impl Clone for Step { } } -/// Type of the hook, executed before or after all [`Scenario`]'s [`Step`]s. +/// Type of a hook executed before or after all [`Scenario`]'s [`Step`]s. /// /// [`Scenario`]: gherkin::Scenario /// [`Step`]: gherkin::Step #[derive(Clone, Copy, Debug)] -pub enum HookTy { - /// Hook, executed on every [`Scenario`] before any [`Step`]s. +pub enum HookType { + /// Executing on each [`Scenario`] before running all [`Step`]s. /// /// [`Scenario`]: gherkin::Scenario /// [`Step`]: gherkin::Step Before, - /// Hook, executed on every [`Scenario`] after all [`Step`]s. + /// Executing on each [`Scenario`] after running all [`Step`]s. /// /// [`Scenario`]: gherkin::Scenario /// [`Step`]: gherkin::Step After, } -/// [`Before`] or [`After`] hook event. +impl fmt::Display for HookType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Event of running [`Before`] or [`After`] hook. /// -/// [`After`]: HookTy::After -/// [`Before`]: HookTy::Before +/// [`After`]: HookType::After +/// [`Before`]: HookType::Before #[derive(Debug)] pub enum Hook { /// Hook execution being started. @@ -259,24 +265,14 @@ pub enum Hook { Failed(Option>, Info), } -impl fmt::Display for HookTy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - HookTy::Before => "Before", - HookTy::After => "After", - }; - write!(f, "{}", s) - } -} - // Manual implementation is required to omit the redundant `World: Clone` trait // bound imposed by `#[derive(Clone)]`. impl Clone for Hook { fn clone(&self) -> Self { match self { - Hook::Started => Hook::Started, - Hook::Passed => Hook::Passed, - Hook::Failed(w, i) => Hook::Failed(w.clone(), i.clone()), + Self::Started => Self::Started, + Self::Passed => Self::Passed, + Self::Failed(w, i) => Self::Failed(w.clone(), i.clone()), } } } @@ -292,7 +288,7 @@ pub enum Scenario { Started, /// [`Hook`] event. - Hook(HookTy, Hook), + Hook(HookType, Hook), /// [`Background`] [`Step`] event. /// @@ -329,7 +325,7 @@ impl Scenario { /// /// [`Scenario`]: gherkin::Scenario #[must_use] - pub fn hook_started(which: HookTy) -> Self { + pub fn hook_started(which: HookType) -> Self { Self::Hook(which, Hook::Started) } @@ -337,7 +333,7 @@ impl Scenario { /// /// [`Scenario`]: gherkin::Scenario #[must_use] - pub fn hook_passed(which: HookTy) -> Self { + pub fn hook_passed(which: HookType) -> Self { Self::Hook(which, Hook::Passed) } @@ -346,7 +342,7 @@ impl Scenario { /// [`Scenario`]: gherkin::Scenario #[must_use] pub fn hook_failed( - which: HookTy, + which: HookType, world: Option>, info: Info, ) -> Self { diff --git a/src/runner/basic.rs b/src/runner/basic.rs index a9dc67d0..ef119ddb 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -13,6 +13,7 @@ use std::{ cmp, collections::HashMap, + convert::identity, fmt, mem, panic::{self, AssertUnwindSafe}, path::PathBuf, @@ -35,7 +36,7 @@ use itertools::Itertools as _; use regex::{CaptureLocations, Regex}; use crate::{ - event::{self, HookTy, Info}, + event::{self, HookType, Info}, feature::Ext as _, parser, step, Runner, Step, World, }; @@ -57,6 +58,40 @@ pub enum ScenarioType { Concurrent, } +/// Alias for [`fn`] used to determine whether a [`Scenario`] is [`Concurrent`] +/// or a [`Serial`] one. +/// +/// [`Concurrent`]: ScenarioType::Concurrent +/// [`Serial`]: ScenarioType::Serial +/// [`Scenario`]: gherkin::Scenario +pub type WhichScenarioFn = fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, +) -> ScenarioType; + +/// Alias for [`fn`] executed on each [`Scenario`] before running all [`Step`]s. +/// +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +pub type BeforeHookFn = for<'a> fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut World, +) -> LocalBoxFuture<'a, ()>; + +/// Alias for [`fn`] executed on each [`Scenario`] after running all [`Step`]s. +/// +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +pub type AfterHookFn = for<'a> fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + Option<&'a mut World>, +) -> LocalBoxFuture<'a, ()>; + /// Default [`Runner`] implementation which follows [_order guarantees_][1] from /// the [`Runner`] trait docs. /// @@ -90,7 +125,7 @@ pub struct Basic< /// [`Scenario`]: gherkin::Scenario which_scenario: F, - /// Function, executed on every [`Scenario`] before any [`Step`]s, + /// Function, executed on each [`Scenario`] before running all [`Step`]s, /// including [`Background`] ones. /// /// [`Background`]: gherkin::Background @@ -98,7 +133,7 @@ pub struct Basic< /// [`Step`]: gherkin::Step before_hook: Option, - /// Function, executed on every [`Scenario`] after all [`Step`]s. + /// Function, executed on each [`Scenario`] after running all [`Step`]s. /// /// [`Background`]: gherkin::Background /// [`Scenario`]: gherkin::Scenario @@ -106,40 +141,6 @@ pub struct Basic< after_hook: Option, } -/// Alias for [`fn`] used to determine whether a [`Scenario`] is [`Concurrent`] -/// or a [`Serial`] one. -/// -/// [`Concurrent`]: ScenarioType::Concurrent -/// [`Serial`]: ScenarioType::Serial -/// [`Scenario`]: gherkin::Scenario -pub type WhichScenarioFn = fn( - &gherkin::Feature, - Option<&gherkin::Rule>, - &gherkin::Scenario, -) -> ScenarioType; - -/// Alias for [`fn`] executed on every [`Scenario`] before any [`Step`]s. -/// -/// [`Scenario`]: gherkin::Scenario -/// [`Step`]: gherkin::Step -pub type BeforeHookFn = for<'a> fn( - &'a gherkin::Feature, - Option<&'a gherkin::Rule>, - &'a gherkin::Scenario, - &'a mut World, -) -> LocalBoxFuture<'a, ()>; - -/// Alias for [`fn`] executed on every [`Scenario`] after all [`Step`]s. -/// -/// [`Scenario`]: gherkin::Scenario -/// [`Step`]: gherkin::Step -pub type AfterHookFn = for<'a> fn( - &'a gherkin::Feature, - Option<&'a gherkin::Rule>, - &'a gherkin::Scenario, - Option<&'a mut World>, -) -> LocalBoxFuture<'a, ()>; - // Implemented manually to omit redundant trait bounds on `World` and to omit // outputting `F`. impl fmt::Debug for Basic { @@ -167,7 +168,7 @@ impl Basic { impl Default for Basic { fn default() -> Self { - let which: WhichScenarioFn = |_, _, scenario| { + let which_scenario: WhichScenarioFn = |_, _, scenario| { scenario .tags .iter() @@ -179,7 +180,7 @@ impl Default for Basic { Self { max_concurrent_scenarios: Some(64), steps: step::Collection::new(), - which_scenario: which, + which_scenario, before_hook: None, after_hook: None, } @@ -232,8 +233,8 @@ impl Basic { } } - /// Sets hook, executed on every [`Scenario`] before any [`Step`]s, - /// including [`Background`] ones. + /// Sets a hook, executed on each [`Scenario`] before running all its + /// [`Step`]s, including [`Background`] ones. /// /// [`Background`]: gherkin::Background /// [`Scenario`]: gherkin::Scenario @@ -264,16 +265,15 @@ impl Basic { } } - /// Sets hook, executed on every [`Scenario`] after all [`Step`]s even after - /// [`Skipped`] of [`Failed`] [`Step`]s. - /// - /// Last `World` argument is supplied to the function, in case it - /// was initialized before by [`before`] hook or any non-failed [`Step`]. - /// In case last [`Scenario`]'s [`Step`] failed, we want to return event - /// with exact `World` state. Also we don't want to impose additional - /// [`Clone`] bounds on `World`, so the only option left is to pass [`None`] - /// to the function. + /// Sets hook, executed on each [`Scenario`] after running all its + /// [`Step`]s, even after [`Skipped`] of [`Failed`] ones. /// + /// Last `World` argument is supplied to the function, in case it was + /// initialized before by running [`before`] hook or any non-failed + /// [`Step`]. In case the last [`Scenario`]'s [`Step`] failed, we want to + /// return event with an exact `World` state. Also, we don't want to impose + /// additional [`Clone`] bounds on `World`, so the only option left is to + /// pass [`None`] to the function. /// /// [`before`]: Self::before() /// [`Failed`]: event::Step::Failed @@ -542,7 +542,7 @@ struct Executor { /// [`Step`]: step::Step collection: step::Collection, - /// Function, executed on every [`Scenario`] before any [`Step`]s, + /// Function, executed on each [`Scenario`] before running all [`Step`]s, /// including [`Background`] ones. /// /// [`Background`]: gherkin::Background @@ -550,9 +550,8 @@ struct Executor { /// [`Step`]: gherkin::Step before_hook: Option, - /// Function, executed on every [`Scenario`] after all [`Step`]s. + /// Function, executed on each [`Scenario`] after running all [`Step`]s. /// - /// [`Background`]: gherkin::Background /// [`Scenario`]: gherkin::Scenario /// [`Step`]: gherkin::Step after_hook: Option, @@ -608,34 +607,32 @@ where /// [`Feature`]: gherkin::Feature /// [`Rule`]: gherkin::Rule /// [`Scenario`]: gherkin::Scenario + #[allow(clippy::too_many_lines)] async fn run_scenario( &self, - f: Arc, - r: Option>, - s: Arc, + feature: Arc, + rule: Option>, + scenario: Arc, ) { - use event::{ - Cucumber, - Scenario::{Finished, Started}, - }; - let ok = |e: fn(Arc) -> event::Scenario| { - let (f, r, s) = (&f, &r, &s); + let (f, r, s) = (&feature, &rule, &scenario); move |step| { - Cucumber::scenario(f.clone(), r.clone(), s.clone(), e(step)) + let (f, r, s) = (f.clone(), r.clone(), s.clone()); + event::Cucumber::scenario(f, r, s, e(step)) } }; let ok_capt = |e: fn(Arc, _) -> event::Scenario| { - let (f, r, s) = (&f, &r, &s); - move |st, cap| { - Cucumber::scenario(f.clone(), r.clone(), s.clone(), e(st, cap)) + let (f, r, s) = (&feature, &rule, &scenario); + move |step, captures| { + let (f, r, s) = (f.clone(), r.clone(), s.clone()); + event::Cucumber::scenario(f, r, s, e(step, captures)) } }; let err = |e: fn(Arc, _, _, _) -> event::Scenario| { - let (f, r, s) = (&f, &r, &s); + let (f, r, s) = (&feature, &rule, &scenario); move |step, captures, w, info| { let (f, r, s) = (f.clone(), r.clone(), s.clone()); - Cucumber::scenario(f, r, s, e(step, captures, w, info)) + event::Cucumber::scenario(f, r, s, e(step, captures, w, info)) } }; @@ -655,15 +652,20 @@ where event::Scenario::step_failed, ); - self.send(Cucumber::scenario(f.clone(), r.clone(), s.clone(), Started)); + self.send(event::Cucumber::scenario( + feature.clone(), + rule.clone(), + scenario.clone(), + event::Scenario::Started, + )); - let res = async { + let world = async { let before_hook = self - .run_before_hook(&f, r.as_ref(), &s) + .run_before_hook(&feature, rule.as_ref(), &scenario) .await .map_err(|_| None)?; - let feature_background = f + let feature_background = feature .background .as_ref() .map(|b| b.steps.iter().map(|s| Arc::new(s.clone()))) @@ -677,7 +679,7 @@ where }) .await?; - let rule_background = r + let rule_background = rule .as_ref() .map(|rule| { rule.background @@ -696,39 +698,39 @@ where }) .await?; - stream::iter(s.steps.iter().map(|s| Arc::new(s.clone()))) + stream::iter(scenario.steps.iter().map(|s| Arc::new(s.clone()))) .map(Ok) .try_fold(rule_background, |world, step| { self.run_step(world, step, into_step_ev).map_ok(Some) }) .await - }; - - let world = match res.await { - Ok(world) | Err(world) => world, - }; - - drop(self.run_after_hook(world, &f, r.as_ref(), &s).await); - - self.send(Cucumber::scenario( - f.clone(), - r.clone(), - s.clone(), - Finished, + } + .await + .unwrap_or_else(identity); + + self.run_after_hook(world, &feature, rule.as_ref(), &scenario) + .await + .map_or((), drop); + + self.send(event::Cucumber::scenario( + feature.clone(), + rule.clone(), + scenario.clone(), + event::Scenario::Finished, )); - if let Some(rule) = r { - if let Some(fin) = self.rule_scenario_finished(f.clone(), rule) { - self.send(fin); + if let Some(r) = rule { + if let Some(f) = self.rule_scenario_finished(feature.clone(), r) { + self.send(f); } } - if let Some(fin) = self.feature_scenario_finished(f) { - self.send(fin); + if let Some(f) = self.feature_scenario_finished(feature) { + self.send(f); } } - /// Executes [`HookTy::Before`], if present. + /// Executes [`HookType::Before`], if present. async fn run_before_hook( &self, feature: &Arc, @@ -738,7 +740,6 @@ where let init_world = async { let world_fut = async { W::new().await.expect("failed to initialize World") }; - AssertUnwindSafe(world_fut) .catch_unwind() .await @@ -750,7 +751,7 @@ where feature.clone(), rule.map(Arc::clone), scenario.clone(), - event::Scenario::hook_started(HookTy::Before), + event::Scenario::hook_started(HookType::Before), )); let fut = init_world.and_then(|mut world| async { @@ -760,19 +761,19 @@ where scenario.as_ref(), &mut world, ); - return match AssertUnwindSafe(fut).catch_unwind().await { + match AssertUnwindSafe(fut).catch_unwind().await { Ok(()) => Ok(world), Err(info) => Err((info, Some(world))), - }; + } }); - return match fut.await { + match fut.await { Ok(world) => { self.send(event::Cucumber::scenario( feature.clone(), rule.map(Arc::clone), scenario.clone(), - event::Scenario::hook_passed(HookTy::Before), + event::Scenario::hook_passed(HookType::Before), )); Ok(Some(world)) } @@ -782,20 +783,20 @@ where rule.map(Arc::clone), scenario.clone(), event::Scenario::hook_failed( - HookTy::Before, + HookType::Before, world.map(Arc::new), info.into(), ), )); Err(()) } - }; + } + } else { + Ok(None) } - - Ok(None) } - /// Executes [`HookTy::After`], if present. + /// Executes [`HookType::After`], if present. async fn run_after_hook( &self, mut world: Option, @@ -808,7 +809,7 @@ where feature.clone(), rule.map(Arc::clone), scenario.clone(), - event::Scenario::hook_started(HookTy::After), + event::Scenario::hook_started(HookType::After), )); let fut = async { @@ -818,19 +819,19 @@ where scenario.as_ref(), world.as_mut(), ); - return match AssertUnwindSafe(fut).catch_unwind().await { + match AssertUnwindSafe(fut).catch_unwind().await { Ok(()) => Ok(world), Err(info) => Err((info, world)), - }; + } }; - return match fut.await { + match fut.await { Ok(world) => { self.send(event::Cucumber::scenario( feature.clone(), rule.map(Arc::clone), scenario.clone(), - event::Scenario::hook_passed(HookTy::After), + event::Scenario::hook_passed(HookType::After), )); Ok(world) } @@ -840,17 +841,17 @@ where rule.map(Arc::clone), scenario.clone(), event::Scenario::hook_failed( - HookTy::After, + HookType::After, world.map(Arc::new), info.into(), ), )); Err(()) } - }; + } + } else { + Ok(None) } - - Ok(None) } /// Runs a [`Step`]. diff --git a/src/writer/basic.rs b/src/writer/basic.rs index b44230ca..6246c89b 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -129,7 +129,7 @@ impl Basic { /// Outputs [started] [`Feature`] to STDOUT. /// /// [started]: event::Feature::Started - /// [`Feature`]: [`gherkin::Feature`] + /// [`Feature`]: gherkin::Feature fn feature_started(&mut self, feature: &gherkin::Feature) { self.lines_to_clear = 1; self.write_line( @@ -169,7 +169,7 @@ impl Basic { /// Outputs [started] [`Rule`] to STDOUT. /// /// [started]: event::Rule::Started - /// [`Rule`]: [`gherkin::Rule`] + /// [`Rule`]: gherkin::Rule fn rule_started(&mut self, rule: &gherkin::Rule) { self.lines_to_clear = 1; self.indent += 2; @@ -187,6 +187,7 @@ impl Basic { /// [background]: event::Background /// [started]: event::Scenario::Started /// [step]: event::Step + /// [`Scenario`]: gherkin::Scenario fn scenario( &mut self, feat: &gherkin::Feature, @@ -219,18 +220,22 @@ impl Basic { } } + /// Outputs [failed] [`Scenario`]'s hook to STDOUT. + /// + /// [failed]: event::Hook::Failed + /// [`Scenario`]: gherkin::Scenario fn hook_failed( &mut self, feat: &gherkin::Feature, sc: &gherkin::Scenario, - which: event::HookTy, + which: event::HookType, world: Option<&W>, info: &Info, ) { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.err(format!( - "{indent}\u{2718} {} Scenario hook failed {}:{}:{}\n\ + "{indent}\u{2718} Scenario's {} hook failed {}:{}:{}\n\ {indent} Captured output: {}{}", which, feat.path @@ -249,7 +254,7 @@ impl Basic { /// Outputs [started] [`Scenario`] to STDOUT. /// /// [started]: event::Scenario::Started - /// [`Scenario`]: [`gherkin::Scenario`] + /// [`Scenario`]: gherkin::Scenario fn scenario_started(&mut self, scenario: &gherkin::Scenario) { self.lines_to_clear = 1; self.indent += 2; @@ -268,7 +273,7 @@ impl Basic { /// [passed]: event::Step::Passed /// [skipped]: event::Step::Skipped /// [started]: event::Step::Started - /// [`Step`]: [`gherkin::Step`] + /// [`Step`]: gherkin::Step fn step( &mut self, feat: &gherkin::Feature, @@ -305,7 +310,7 @@ impl Basic { /// [passed]: event::Step::Passed /// [skipped]: event::Step::Skipped /// [started]: event::Step::Started - /// [`Step`]: [`gherkin::Step`] + /// [`Step`]: gherkin::Step fn step_started(&mut self, step: &gherkin::Step) { self.indent += 4; if self.styles.is_present { @@ -327,7 +332,7 @@ impl Basic { /// Outputs [passed] [`Step`] to STDOUT. /// /// [passed]: event::Step::Passed - /// [`Step`]: [`gherkin::Step`] + /// [`Step`]: gherkin::Step fn step_passed( &mut self, step: &gherkin::Step, @@ -362,7 +367,7 @@ impl Basic { /// Outputs [skipped] [`Step`] to STDOUT. /// /// [skipped]: event::Step::Skipped - /// [`Step`]: [`gherkin::Step`] + /// [`Step`]: gherkin::Step fn step_skipped(&mut self, feat: &gherkin::Feature, step: &gherkin::Step) { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.skipped(format!( @@ -388,7 +393,7 @@ impl Basic { /// Outputs [failed] [`Step`] to STDOUT. /// /// [failed]: event::Step::Failed - /// [`Step`]: [`gherkin::Step`] + /// [`Step`]: gherkin::Step fn step_failed( &mut self, feat: &gherkin::Feature, @@ -450,8 +455,8 @@ impl Basic { /// [passed]: event::Step::Passed /// [skipped]: event::Step::Skipped /// [started]: event::Step::Started - /// [`Background`]: [`gherkin::Background`] - /// [`Step`]: [`gherkin::Step`] + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step fn background( &mut self, feat: &gherkin::Feature, @@ -488,8 +493,8 @@ impl Basic { /// [passed]: event::Step::Passed /// [skipped]: event::Step::Skipped /// [started]: event::Step::Started - /// [`Background`]: [`gherkin::Background`] - /// [`Step`]: [`gherkin::Step`] + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step fn bg_step_started(&mut self, step: &gherkin::Step) { self.indent += 4; if self.styles.is_present { @@ -511,8 +516,8 @@ impl Basic { /// Outputs [passed] [`Background`] [`Step`] to STDOUT. /// /// [passed]: event::Step::Passed - /// [`Background`]: [`gherkin::Background`] - /// [`Step`]: [`gherkin::Step`] + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step fn bg_step_passed( &mut self, step: &gherkin::Step, @@ -547,8 +552,8 @@ impl Basic { /// Outputs [skipped] [`Background`] [`Step`] to STDOUT. /// /// [skipped]: event::Step::Skipped - /// [`Background`]: [`gherkin::Background`] - /// [`Step`]: [`gherkin::Step`] + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step fn bg_step_skipped( &mut self, feat: &gherkin::Feature, @@ -578,8 +583,8 @@ impl Basic { /// Outputs [failed] [`Background`] [`Step`] to STDOUT. /// /// [failed]: event::Step::Failed - /// [`Background`]: [`gherkin::Background`] - /// [`Step`]: [`gherkin::Step`] + /// [`Background`]: gherkin::Background + /// [`Step`]: gherkin::Step fn bg_step_failed( &mut self, feat: &gherkin::Feature, diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index 77da0149..d886f7b2 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -57,6 +57,34 @@ impl Stats { } } +/// Alias for [`fn`] used to determine should [`Skipped`] test considered as +/// [`Failed`] or not. +/// +/// [`Failed`]: event::Step::Failed +/// [`Skipped`]: event::Step::Skipped +pub type SkipFn = + fn(&gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario) -> bool; + +/// Indicator of a [`Failed`] or [`Skipped`] [`Scenario`]. +/// +/// [`Failed`]: event::Step::Failed +/// [`Scenario`]: gherkin::Scenario +/// [`Skipped`]: event::Step::Skipped +#[derive(Clone, Copy, Debug)] +enum Indicator { + /// [`Failed`] [`Scenario`]. + /// + /// [`Failed`]: event::Step::Failed + /// [`Scenario`]: gherkin::Scenario + Failed, + + /// [`Skipped`] [`Scenario`]. + /// + /// [`Scenario`]: gherkin::Scenario + /// [`Skipped`]: event::Step::Skipped + Skipped, +} + /// Wrapper for a [`Writer`] for outputting an execution summary (number of /// executed features, scenarios, steps and parsing errors). /// @@ -104,35 +132,7 @@ pub struct Summarized { /// Handled [`Scenario`]s to collect [`Stats`]. /// /// [`Scenario`]: gherkin::Scenario - handled_scenarios: HashMap, FailedOrSkipped>, -} - -/// Alias for [`fn`] used to determine should [`Skipped`] test considered as -/// [`Failed`] or not. -/// -/// [`Failed`]: event::Step::Failed -/// [`Skipped`]: event::Step::Skipped -pub type SkipFn = - fn(&gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario) -> bool; - -/// [`Failed`] or [`Skipped`] [`Scenario`]s. -/// -/// [`Failed`]: event::Step::Failed -/// [`Scenario`]: gherkin::Scenario -/// [`Skipped`]: event::Step::Skipped -#[derive(Clone, Copy, Debug)] -enum FailedOrSkipped { - /// [`Failed`] [`Scenario`]. - /// - /// [`Failed`]: event::Step::Failed - /// [`Scenario`]: gherkin::Scenario - Failed, - - /// [`Skipped`] [`Scenario`]. - /// - /// [`Scenario`]: gherkin::Scenario - /// [`Skipped`]: event::Step::Skipped - Skipped, + handled_scenarios: HashMap, Indicator>, } #[async_trait(?Send)] @@ -238,7 +238,7 @@ impl Summarized { ) { use self::{ event::Step, - FailedOrSkipped::{Failed, Skipped}, + Indicator::{Failed, Skipped}, }; match ev { @@ -279,8 +279,8 @@ impl Summarized { // - If Scenario executed no Steps and then Hook failed, we // track Scenario as failed. match self.handled_scenarios.get(scenario) { - Some(FailedOrSkipped::Failed) => {} - Some(FailedOrSkipped::Skipped) => { + Some(Indicator::Failed) => {} + Some(Indicator::Skipped) => { self.scenarios.skipped -= 1; self.scenarios.failed += 1; } @@ -288,7 +288,7 @@ impl Summarized { self.scenarios.failed += 1; let _ = self .handled_scenarios - .insert(scenario.clone(), FailedOrSkipped::Failed); + .insert(scenario.clone(), Indicator::Failed); } } self.failed_hooks += 1; @@ -345,11 +345,11 @@ impl Styles { .unwrap_or_default(); let comma = (!parsing_errors.is_empty() && !hook_errors.is_empty()) - .then(|| self.err(",")) + .then(|| self.err(", ")) .unwrap_or_default(); format!( - "{}\n{}\n{}{}{}\n{}{}\n{}{} {}", + "{}\n{}\n{}{}{}\n{}{}\n{}{}{}", self.bold(self.header("[Summary]")), features, rules, diff --git a/tests/wait.rs b/tests/wait.rs index 08c6a79c..ec87e380 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -26,7 +26,7 @@ async fn main() { .expect_err("should err"); let err = err.downcast_ref::().unwrap(); - assert_eq!(err, "2 steps failed, 1 parsing error, 0 hook errors"); + assert_eq!(err, "2 steps failed, 1 parsing error"); } #[given(regex = r"(\d+) secs?")] From 609c29d78600e1a86f7a662d6ef54eb93c009886 Mon Sep 17 00:00:00 2001 From: ilslv Date: Mon, 18 Oct 2021 14:01:38 +0300 Subject: [PATCH 7/9] Fix clap at beta.5 to prevent updating it with breaking changes --- Cargo.toml | 2 +- src/cli.rs | 3 +-- src/cucumber.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a1b6f681..aa8f9fe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ macros = ["cucumber-codegen", "inventory"] [dependencies] async-trait = "0.1.40" atty = "0.2.14" -clap = "3.0.0-beta.4" +clap = "=3.0.0-beta.5" console = "0.14.1" derive_more = { version = "0.99.16", features = ["deref", "display", "error", "from"], default_features = false } either = "1.6" diff --git a/src/cli.rs b/src/cli.rs index 5e604181..79640e04 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,6 @@ //! CLI options. -use clap::Clap; use regex::Regex; /// Run the tests, pet a dog!. @@ -20,7 +19,7 @@ use regex::Regex; /// [cucumber-rs/cucumber#134][1]. /// /// [1]: https://github.com/cucumber-rs/cucumber/issues/134 -#[derive(Clap, Debug)] +#[derive(Debug, clap::Parser)] pub struct Opts { /// Regex to select scenarios from. #[clap(short = 'e', long = "expression", name = "regex")] diff --git a/src/cucumber.rs b/src/cucumber.rs index 0087c0e1..f0451049 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -20,7 +20,7 @@ use std::{ path::Path, }; -use clap::Clap as _; +use clap::Parser as _; use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; From 54b714e90d5c77825f22adc84d2653f69c2709e3 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 18 Oct 2021 13:59:53 +0300 Subject: [PATCH 8/9] Fix `clap` to `3.0.0-beta.5` version --- Cargo.toml | 2 +- src/cli.rs | 2 +- src/cucumber.rs | 134 +++++++++--------------------------------------- 3 files changed, 26 insertions(+), 112 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa8f9fe4..139555d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ macros = ["cucumber-codegen", "inventory"] [dependencies] async-trait = "0.1.40" atty = "0.2.14" -clap = "=3.0.0-beta.5" +clap = { version = "=3.0.0-beta.5", features = ["derive"] } console = "0.14.1" derive_more = { version = "0.99.16", features = ["deref", "display", "error", "from"], default_features = false } either = "1.6" diff --git a/src/cli.rs b/src/cli.rs index 79640e04..0106fb09 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,7 +19,7 @@ use regex::Regex; /// [cucumber-rs/cucumber#134][1]. /// /// [1]: https://github.com/cucumber-rs/cucumber/issues/134 -#[derive(Debug, clap::Parser)] +#[derive(clap::Parser, Debug)] pub struct Opts { /// Regex to select scenarios from. #[clap(short = 'e', long = "expression", name = "regex")] diff --git a/src/cucumber.rs b/src/cucumber.rs index f0451049..db5cfe53 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -21,7 +21,7 @@ use std::{ }; use clap::Parser as _; -use futures::{future::LocalBoxFuture, StreamExt as _}; +use futures::StreamExt as _; use regex::Regex; use crate::{ @@ -774,9 +774,22 @@ where I: AsRef, { fn default() -> Self { + let which: runner::basic::WhichScenarioFn = |_, _, scenario| { + scenario + .tags + .iter() + .any(|tag| tag == "serial") + .then(|| ScenarioType::Serial) + .unwrap_or(ScenarioType::Concurrent) + }; + Cucumber::custom() .with_parser(parser::Basic::new()) - .with_runner(runner::Basic::default()) + .with_runner( + runner::Basic::custom() + .which_scenario(which) + .max_concurrent_scenarios(64), + ) .with_writer(writer::Basic::new().normalized().summarized()) } } @@ -827,7 +840,7 @@ impl Cucumber { } } -impl Cucumber, Wr> { +impl Cucumber, Wr> { /// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will /// be limited. /// @@ -851,7 +864,7 @@ impl Cucumber, Wr> { pub fn which_scenario( self, func: Which, - ) -> Cucumber, Wr> + ) -> Cucumber, Wr> where Which: Fn( &gherkin::Feature, @@ -875,84 +888,6 @@ impl Cucumber, Wr> { } } - /// Sets a hook, executed on each [`Scenario`] before running all its - /// [`Step`]s, including [`Background`] ones. - /// - /// [`Background`]: gherkin::Background - /// [`Scenario`]: gherkin::Scenario - /// [`Step`]: gherkin::Step - #[must_use] - pub fn before( - self, - func: Before, - ) -> Cucumber, Wr> - where - Before: for<'a> Fn( - &'a gherkin::Feature, - Option<&'a gherkin::Rule>, - &'a gherkin::Scenario, - &'a mut W, - ) -> LocalBoxFuture<'a, ()>, - { - let Self { - parser, - runner, - writer, - .. - } = self; - Cucumber { - parser, - runner: runner.before(func), - writer, - _world: PhantomData, - _parser_input: PhantomData, - } - } - - /// Sets a hook, executed on each [`Scenario`] after running all its - /// [`Step`]s, even after [`Skipped`] of [`Failed`] [`Step`]s. - /// - /// Last `World` argument is supplied to the function, in case it was - /// initialized before by running [`before`] hook or any non-failed - /// [`Step`]. In case the last [`Scenario`]'s [`Step`] failed, we want to - /// return event with an exact `World` state. Also, we don't want to impose - /// additional [`Clone`] bounds on `World`, so the only option left is to - /// pass [`None`] to the function. - /// - /// - /// [`before`]: Self::before() - /// [`Failed`]: event::Step::Failed - /// [`Scenario`]: gherkin::Scenario - /// [`Skipped`]: event::Step::Skipped - /// [`Step`]: gherkin::Step - #[must_use] - pub fn after( - self, - func: After, - ) -> Cucumber, Wr> - where - After: for<'a> Fn( - &'a gherkin::Feature, - Option<&'a gherkin::Rule>, - &'a gherkin::Scenario, - Option<&'a mut W>, - ) -> LocalBoxFuture<'a, ()>, - { - let Self { - parser, - runner, - writer, - .. - } = self; - Cucumber { - parser, - runner: runner.after(func), - writer, - _world: PhantomData, - _parser_input: PhantomData, - } - } - /// Replaces [`Collection`] of [`Step`]s. /// /// [`Collection`]: step::Collection @@ -1092,36 +1027,15 @@ where { let writer = self.filter_run(input, filter).await; if writer.execution_has_failed() { - let mut msg = Vec::with_capacity(3); - let failed_steps = writer.failed_steps(); - if failed_steps > 0 { - msg.push(format!( - "{} step{} failed", - failed_steps, - (failed_steps > 1).then(|| "s").unwrap_or_default(), - )); - } - let parsing_errors = writer.parsing_errors(); - if parsing_errors > 0 { - msg.push(format!( - "{} parsing error{}", - parsing_errors, - (parsing_errors > 1).then(|| "s").unwrap_or_default(), - )); - } - - let hook_errors = writer.hook_errors(); - if hook_errors > 0 { - msg.push(format!( - "{} hook error{}", - hook_errors, - (hook_errors > 1).then(|| "s").unwrap_or_default(), - )); - } - - panic!("{}", msg.join(", ")); + panic!( + "{} step{} failed, {} parsing error{}", + failed_steps, + (failed_steps != 1).then(|| "s").unwrap_or_default(), + parsing_errors, + (parsing_errors != 1).then(|| "s").unwrap_or_default(), + ); } } } From 558a9a4e382fa18b682140b209dc8e18973fb74d Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 18 Oct 2021 14:24:44 +0300 Subject: [PATCH 9/9] Restore bad merge --- book/tests/Cargo.toml | 2 +- src/cucumber.rs | 134 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/book/tests/Cargo.toml b/book/tests/Cargo.toml index e01a6c3c..cc74553e 100644 --- a/book/tests/Cargo.toml +++ b/book/tests/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] async-trait = "0.1" -cucumber = { path = "../.." } +cucumber = { version = "0.10", path = "../.." } futures = "0.3" skeptic = "0.13" tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } diff --git a/src/cucumber.rs b/src/cucumber.rs index db5cfe53..f0451049 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -21,7 +21,7 @@ use std::{ }; use clap::Parser as _; -use futures::StreamExt as _; +use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; use crate::{ @@ -774,22 +774,9 @@ where I: AsRef, { fn default() -> Self { - let which: runner::basic::WhichScenarioFn = |_, _, scenario| { - scenario - .tags - .iter() - .any(|tag| tag == "serial") - .then(|| ScenarioType::Serial) - .unwrap_or(ScenarioType::Concurrent) - }; - Cucumber::custom() .with_parser(parser::Basic::new()) - .with_runner( - runner::Basic::custom() - .which_scenario(which) - .max_concurrent_scenarios(64), - ) + .with_runner(runner::Basic::default()) .with_writer(writer::Basic::new().normalized().summarized()) } } @@ -840,7 +827,7 @@ impl Cucumber { } } -impl Cucumber, Wr> { +impl Cucumber, Wr> { /// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will /// be limited. /// @@ -864,7 +851,7 @@ impl Cucumber, Wr> { pub fn which_scenario( self, func: Which, - ) -> Cucumber, Wr> + ) -> Cucumber, Wr> where Which: Fn( &gherkin::Feature, @@ -888,6 +875,84 @@ impl Cucumber, Wr> { } } + /// Sets a hook, executed on each [`Scenario`] before running all its + /// [`Step`]s, including [`Background`] ones. + /// + /// [`Background`]: gherkin::Background + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: gherkin::Step + #[must_use] + pub fn before( + self, + func: Before, + ) -> Cucumber, Wr> + where + Before: for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + &'a mut W, + ) -> LocalBoxFuture<'a, ()>, + { + let Self { + parser, + runner, + writer, + .. + } = self; + Cucumber { + parser, + runner: runner.before(func), + writer, + _world: PhantomData, + _parser_input: PhantomData, + } + } + + /// Sets a hook, executed on each [`Scenario`] after running all its + /// [`Step`]s, even after [`Skipped`] of [`Failed`] [`Step`]s. + /// + /// Last `World` argument is supplied to the function, in case it was + /// initialized before by running [`before`] hook or any non-failed + /// [`Step`]. In case the last [`Scenario`]'s [`Step`] failed, we want to + /// return event with an exact `World` state. Also, we don't want to impose + /// additional [`Clone`] bounds on `World`, so the only option left is to + /// pass [`None`] to the function. + /// + /// + /// [`before`]: Self::before() + /// [`Failed`]: event::Step::Failed + /// [`Scenario`]: gherkin::Scenario + /// [`Skipped`]: event::Step::Skipped + /// [`Step`]: gherkin::Step + #[must_use] + pub fn after( + self, + func: After, + ) -> Cucumber, Wr> + where + After: for<'a> Fn( + &'a gherkin::Feature, + Option<&'a gherkin::Rule>, + &'a gherkin::Scenario, + Option<&'a mut W>, + ) -> LocalBoxFuture<'a, ()>, + { + let Self { + parser, + runner, + writer, + .. + } = self; + Cucumber { + parser, + runner: runner.after(func), + writer, + _world: PhantomData, + _parser_input: PhantomData, + } + } + /// Replaces [`Collection`] of [`Step`]s. /// /// [`Collection`]: step::Collection @@ -1027,15 +1092,36 @@ where { let writer = self.filter_run(input, filter).await; if writer.execution_has_failed() { + let mut msg = Vec::with_capacity(3); + let failed_steps = writer.failed_steps(); + if failed_steps > 0 { + msg.push(format!( + "{} step{} failed", + failed_steps, + (failed_steps > 1).then(|| "s").unwrap_or_default(), + )); + } + let parsing_errors = writer.parsing_errors(); - panic!( - "{} step{} failed, {} parsing error{}", - failed_steps, - (failed_steps != 1).then(|| "s").unwrap_or_default(), - parsing_errors, - (parsing_errors != 1).then(|| "s").unwrap_or_default(), - ); + if parsing_errors > 0 { + msg.push(format!( + "{} parsing error{}", + parsing_errors, + (parsing_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + let hook_errors = writer.hook_errors(); + if hook_errors > 0 { + msg.push(format!( + "{} hook error{}", + hook_errors, + (hook_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + panic!("{}", msg.join(", ")); } } }