Custom Writer
Finally, let's implement a custom Writer
which simply outputs cucumber events to STDOUT in the order of receiving.
Writer
represents anything that consumes 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, WriterExt as _, }; 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> { 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") { 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>> { 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; 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() } } struct CustomWriter; impl<W: 'static> cucumber::Writer<W> for CustomWriter { type Cli = cli::Empty; // we provide no CLI options async fn handle_event( &mut self, ev: parser::Result<Event<event::Cucumber<W>>>, _: &Self::Cli, ) { match ev { Ok(Event { value, .. }) => match value { event::Cucumber::Feature(feature, ev) => match ev { event::Feature::Started => { println!("{}: {}", feature.keyword, feature.name) } event::Feature::Scenario(scenario, ev) => match ev.event { event::Scenario::Started => { println!("{}: {}", scenario.keyword, scenario.name) } event::Scenario::Step(step, ev) => match ev { event::Step::Started => { print!("{} {}...", step.keyword, step.value) } event::Step::Passed(..) => println!("ok"), event::Step::Skipped => println!("skip"), event::Step::Failed(_, _, _, err) => { println!("failed: {err}") } }, _ => {} }, _ => {} }, _ => {} }, Err(e) => println!("Error: {e}"), } } } #[tokio::main] async fn main() { AnimalWorld::cucumber::<&str>() // aiding type inference for `CustomParser` .with_parser(CustomParser) .with_runner(CustomRunner) .with_writer(CustomWriter.assert_normalized()) // OK because of `CustomRunner` .run("tests/features/book") .await; }
TIP:
CustomWriter
will print trash if we feed unorderedevent::Cucumber
s into it. Though, we shouldn't care about order normalization in our implementations. Instead, we may just wrapCustomWriter
intowriter::Normalize
, which will do that for us.
extern crate cucumber; extern crate futures; extern crate tokio; use std::{path::PathBuf, time::Duration}; use cucumber::{ cli, event, gherkin, given, parser, then, when, Event, World, WriterExt as _, }; use futures::{future, stream}; 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 CustomWriter; impl<W: 'static> cucumber::Writer<W> for CustomWriter { type Cli = cli::Empty; // we provide no CLI options async fn handle_event( &mut self, ev: parser::Result<Event<event::Cucumber<W>>>, _: &Self::Cli, ) { match ev { Ok(Event { value, .. }) => match value { event::Cucumber::Feature(feature, ev) => match ev { event::Feature::Started => { println!("{}: {}", feature.keyword, feature.name) } event::Feature::Scenario(scenario, ev) => match ev.event { event::Scenario::Started => { println!("{}: {}", scenario.keyword, scenario.name) } event::Scenario::Step(step, ev) => match ev { event::Step::Started => { print!("{} {}...", step.keyword, step.value) } event::Step::Passed(..) => println!("ok"), event::Step::Skipped => println!("skip"), event::Step::Failed(_, _, _, err) => { println!("failed: {err}", ) } }, _ => {} }, _ => {} }, _ => {} }, Err(e) => println!("Error: {e}"), } } } #[tokio::main] async fn main() { AnimalWorld::cucumber::<&str>() // aiding type inference for `CustomParser` .with_parser(CustomParser) .with_writer(CustomWriter.normalized()) // wrapping into `writer::Normalize`, .run("tests/features/book") // so it works OK with the default .await; // concurrent `Runner` }
NOTE:
Writer
s are easily pipelined. SeeWriterExt
trait andwriter
module for moreWriter
machinery "included batteries".