Custom Runner
Now, let's implement a custom Runner
which simply executes scenarios in features sequentially, without considering tags, rules, Background
s or other extras, and specifically suitable for our AnimalWorld
(for implementation simplicity).
Runner
represents anything that transforms a Stream
of features into a Stream
of cucumber events.
extern crate cucumber; extern crate futures; extern crate tokio; use std::{ panic::{self, AssertUnwindSafe}, path::PathBuf, sync::{Arc, LazyLock}, time::Duration, }; use cucumber::{ cli, event, gherkin, given, parser, step, then, when, Event, World, }; use futures::{ future::{self, FutureExt as _}, stream::{self, LocalBoxStream, Stream, StreamExt as _, TryStreamExt as _}, }; use tokio::time::sleep; #[derive(Clone, Copy, Debug, Default)] struct Animal { pub hungry: bool, } impl Animal { fn feed(&mut self) { self.hungry = false; } } #[derive(Clone, Debug, Default, World)] pub struct AnimalWorld { cat: Animal, } #[given(regex = r"^a (hungry|satiated) cat$")] async fn hungry_cat(world: &mut AnimalWorld, state: String) { sleep(Duration::from_secs(2)).await; match state.as_str() { "hungry" => world.cat.hungry = true, "satiated" => world.cat.hungry = false, _ => unreachable!(), } } #[when("I feed the cat")] async fn feed_cat(world: &mut AnimalWorld) { sleep(Duration::from_secs(2)).await; world.cat.feed(); } #[then("the cat is not hungry")] async fn cat_is_fed(world: &mut AnimalWorld) { sleep(Duration::from_secs(2)).await; assert!(!world.cat.hungry); } struct CustomParser; impl<I> cucumber::Parser<I> for CustomParser { type Cli = cli::Empty; type Output = stream::Once<future::Ready<parser::Result<gherkin::Feature>>>; fn parse(self, _: I, _: Self::Cli) -> Self::Output { let keyword = "Feature"; let name = "Animal feature"; stream::once(future::ok(gherkin::Feature { keyword: keyword.into(), name: name.into(), description: None, background: None, scenarios: vec![gherkin::Scenario { keyword: "Scenario".into(), name: "If we feed a hungry cat it won't be hungry".into(), description: None, steps: vec![ gherkin::Step { keyword: "Given ".into(), ty: gherkin::StepType::Given, value: "a hungry cat".into(), docstring: None, table: None, span: gherkin::Span { start: 5, end: 18 }, position: gherkin::LineCol { line: 3, col: 5 }, }, gherkin::Step { keyword: "When ".into(), ty: gherkin::StepType::When, value: "I feed the cat".into(), docstring: None, table: None, span: gherkin::Span { start: 5, end: 19 }, position: gherkin::LineCol { line: 4, col: 5 }, }, gherkin::Step { keyword: "Then ".into(), ty: gherkin::StepType::Then, value: "the cat is not hungry".into(), docstring: None, table: None, span: gherkin::Span { start: 5, end: 26 }, position: gherkin::LineCol { line: 5, col: 5 }, }, ], examples: vec![], tags: vec![], span: gherkin::Span { start: 3, end: 52 }, position: gherkin::LineCol { line: 2, col: 3 }, }], rules: vec![], tags: vec![], span: gherkin::Span { start: 1, end: 23 }, position: gherkin::LineCol { line: 1, col: 1 }, path: Some(PathBuf::from(file!())), })) } } struct CustomRunner; impl CustomRunner { fn steps_fns() -> &'static step::Collection<AnimalWorld> { // Wire the static collection of step matching functions. static STEPS: LazyLock<step::Collection<AnimalWorld>> = LazyLock::new(AnimalWorld::collection); &STEPS } async fn execute_step( mut world: AnimalWorld, step: gherkin::Step, ) -> (AnimalWorld, event::Step<AnimalWorld>) { let ev = if let Some((step_fn, captures, loc, ctx)) = Self::steps_fns().find(&step).expect("Ambiguous match") { // Panic represents a failed assertion in a step matching // function. match AssertUnwindSafe(step_fn(&mut world, ctx)) .catch_unwind() .await { Ok(()) => event::Step::Passed(captures, loc), Err(e) => event::Step::Failed( Some(captures), loc, Some(Arc::new(world.clone())), event::StepError::Panic(e.into()), ), } } else { event::Step::Skipped }; (world, ev) } async fn execute_scenario( scenario: gherkin::Scenario, ) -> impl Stream<Item = event::Feature<AnimalWorld>> { // Those panic hook shenanigans are done to avoid console messages like // "thread 'main' panicked at ..." // // 1. We obtain the current panic hook and replace it with an empty one. // 2. We run tests, which can panic. In that case we pass all panic info // down the line to the `Writer`, which will print it at right time. // 3. We restore original panic hook, because suppressing all panics // doesn't sound like a very good idea. let hook = panic::take_hook(); panic::set_hook(Box::new(|_| {})); let mut world = AnimalWorld::new().await.unwrap(); let mut steps = Vec::with_capacity(scenario.steps.len()); for step in scenario.steps.clone() { let (w, ev) = Self::execute_step(world, step.clone()).await; world = w; let should_stop = matches!(ev, event::Step::Failed(..)); steps.push((step, ev)); if should_stop { break; } } panic::set_hook(hook); let scenario = Arc::new(scenario); stream::once(future::ready(event::Scenario::Started)) .chain(stream::iter(steps.into_iter().flat_map(|(step, ev)| { let step = Arc::new(step); [ event::Scenario::Step(step.clone(), event::Step::Started), event::Scenario::Step(step, ev), ] }))) .chain(stream::once(future::ready(event::Scenario::Finished))) .map(move |event| event::Feature::Scenario( scenario.clone(), event::RetryableScenario { event, retries: None }, )) } fn execute_feature( feature: gherkin::Feature, ) -> impl Stream<Item = event::Cucumber<AnimalWorld>> { let feature = Arc::new(feature); stream::once(future::ready(event::Feature::Started)) .chain( stream::iter(feature.scenarios.clone()) .then(Self::execute_scenario) .flatten(), ) .chain(stream::once(future::ready(event::Feature::Finished))) .map(move |ev| event::Cucumber::Feature(feature.clone(), ev)) } } impl cucumber::Runner<AnimalWorld> for CustomRunner { type Cli = cli::Empty; // we provide no CLI options type EventStream = LocalBoxStream< 'static, parser::Result<Event<event::Cucumber<AnimalWorld>>>, >; fn run<S>(self, features: S, _: Self::Cli) -> Self::EventStream where S: Stream<Item = parser::Result<gherkin::Feature>> + 'static, { stream::once(future::ok(event::Cucumber::Started)) .chain( features .map_ok(|f| Self::execute_feature(f).map(Ok)) .try_flatten(), ) .chain(stream::once(future::ok(event::Cucumber::Finished))) .map_ok(Event::new) .boxed_local() } } #[tokio::main] async fn main() { AnimalWorld::cucumber::<&str>() // aiding type inference for `CustomParser` .with_parser(CustomParser) .with_runner(CustomRunner) .run_and_exit("tests/features/book") .await; }
NOTE: Output is printed only after all the steps were executed, because we have implemented the
CustomRunner
in the way to emitevent::Cucumber
s only after executing all the scenario steps (seeexecute_scenario()
function).