diff --git a/CHANGELOG.md b/CHANGELOG.md index ae10c60a..4f729275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All user visible changes to `cucumber` crate will be documented in this file. Th ### BC Breaks - Moved `World` type parameter of `WriterExt` trait to methods. ([#160]) +- Renamed `Normalized` and `Summarized` `Writer`s to `Normalize` and `Summarize`. ([#162]) +- Removed `writer::Basic` `Default` impl and change `writer::Basic::new()` return type to `writer::Normalize`. ([#162]) ### Added @@ -23,6 +25,10 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - `writer::Json` ([Cucumber JSON format][0110-2]) behind the `output-json` feature flag. ([#159]) - `writer::Tee` for outputting to multiple terminating `Writer`s simultaneously. ([#160]) - `writer::discard::Arbitrary` and `writer::discard::Failure` for providing no-op implementations of the corresponding `Writer` traits. ([#160]) +- Inability to build invalid `Writer`s pipelines: + - `writer::Normalized` trait required for `Writer`s in `Cucumber` running methods. ([#162]) + - `writer::NonTransforming` trait required for `writer::Repeat`. ([#162]) + - `writer::Summarizable` trait required for `writer::Summarize`. ([#162]) ### Fixed @@ -32,6 +38,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [#151]: /../../pull/151 [#159]: /../../pull/159 [#160]: /../../pull/160 +[#162]: /../../pull/162 [#163]: /../../pull/163 [0110-1]: https://llg.cubic.org/docs/junit [0110-2]: https://github.com/cucumber/cucumber-json-schema diff --git a/book/src/Features.md b/book/src/Features.md index 5e54c876..5723e80e 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -508,10 +508,11 @@ use cucumber::{writer, WriterExt as _}; let file = fs::File::create(dbg!(format!("{}/target/schema.json", env!("CARGO_MANIFEST_DIR"))))?; World::cucumber() .with_writer( - writer::Basic::default() - .summarized() - .tee::(writer::Json::for_tee(file)) - .normalized(), + // `Writer`s pipeline is constructed in a reversed order. + writer::Basic::stdout() // And output to STDOUT. + .summarized() // Simultaneously, add execution summary. + .tee::(writer::Json::for_tee(file)) // Then, output to JSON file. + .normalized() // First, normalize events order. ) .run_and_exit("tests/features/book") .await; diff --git a/src/cli.rs b/src/cli.rs index 9b169374..1ff0d56a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -173,9 +173,7 @@ This struct is especially useful, when implementing custom [`Writer`] wrapping another one: ```rust # use async_trait::async_trait; -# use cucumber::{ -# cli, event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, -# }; +# use cucumber::{cli, event, parser, writer, Event, World, Writer}; # use structopt::StructOpt; # struct CustomWriter(Wr); @@ -208,11 +206,11 @@ where // Useful blanket impls: #[async_trait(?Send)] -impl<'val, W, Wr, Val> ArbitraryWriter<'val, W, Val> for CustomWriter +impl<'val, W, Wr, Val> writer::Arbitrary<'val, W, Val> for CustomWriter where W: World, Self: Writer, - Wr: ArbitraryWriter<'val, W, Val>, + Wr: writer::Arbitrary<'val, W, Val>, Val: 'val, { async fn write(&mut self, val: Val) @@ -223,11 +221,11 @@ where } } -impl FailureWriter for CustomWriter +impl writer::Failure for CustomWriter where W: World, Self: Writer, - Wr: FailureWriter, + Wr: writer::Failure, { fn failed_steps(&self) -> usize { self.0.failed_steps() @@ -241,6 +239,12 @@ where self.0.hook_errors() } } + +impl writer::Normalized for CustomWriter {} + +impl writer::NonTransforming + for CustomWriter +{} ``` [`Writer`]: crate::Writer diff --git a/src/cucumber.rs b/src/cucumber.rs index c20c05cd..64b52e31 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -25,9 +25,8 @@ use regex::Regex; use structopt::{StructOpt, StructOptInternal}; use crate::{ - cli, event, parser, runner, step, tag::Ext as _, writer, Event, - FailureWriter, Parser, Runner, ScenarioType, Step, World, Writer, - WriterExt as _, + cli, event, parser, runner, step, tag::Ext as _, writer, Event, Parser, + Runner, ScenarioType, Step, World, Writer, WriterExt as _, }; /// Top-level [Cucumber] executor. @@ -214,7 +213,10 @@ where #[must_use] pub fn repeat_skipped( self, - ) -> Cucumber, Cli> { + ) -> Cucumber, Cli> + where + Wr: writer::NonTransforming, + { Cucumber { parser: self.parser, runner: self.runner, @@ -296,20 +298,14 @@ where /// async data-autoplay="true" data-rows="24"> /// /// - /// > ⚠️ __WARNING__: [`Cucumber::repeat_failed()`] should be called before - /// [`Cucumber::fail_on_skipped()`], as events pass from - /// outer [`Writer`]s to inner ones. So we need to - /// transform [`Skipped`] to [`Failed`] first, and only - /// then [`Repeat`] them. - /// /// [`Failed`]: crate::event::Step::Failed - /// [`Repeat`]: writer::Repeat - /// [`Scenario`]: gherkin::Scenario - /// [`Skipped`]: crate::event::Step::Skipped #[must_use] pub fn repeat_failed( self, - ) -> Cucumber, Cli> { + ) -> Cucumber, Cli> + where + Wr: writer::NonTransforming, + { Cucumber { parser: self.parser, runner: self.runner, @@ -414,16 +410,7 @@ where /// async data-autoplay="true" data-rows="24"> /// /// - /// > ⚠️ __WARNING__: [`Cucumber::repeat_if()`] should be called before - /// [`Cucumber::fail_on_skipped()`], as events pass from - /// outer [`Writer`]s to inner ones. So we need to - /// transform [`Skipped`] to [`Failed`] first, and only - /// then [`Repeat`] them. - /// /// [`Failed`]: crate::event::Step::Failed - /// [`Repeat`]: writer::Repeat - /// [`Scenario`]: gherkin::Scenario - /// [`Skipped`]: crate::event::Step::Skipped #[must_use] pub fn repeat_if( self, @@ -431,6 +418,7 @@ where ) -> Cucumber, Cli> where F: Fn(&parser::Result>>) -> bool, + Wr: writer::NonTransforming, { Cucumber { parser: self.parser, @@ -638,7 +626,7 @@ where W: World, P: Parser, R: Runner, - Wr: Writer, + Wr: Writer + writer::Normalized, Cli: StructOpt + StructOptInternal, { /// Runs [`Cucumber`]. @@ -900,7 +888,7 @@ pub(crate) type DefaultCucumber = Cucumber< parser::Basic, I, runner::Basic, - writer::Summarized>, + writer::Summarize>, >; impl Default for DefaultCucumber @@ -912,7 +900,7 @@ where Self::custom( parser::Basic::new(), runner::Basic::default(), - writer::Basic::default().normalized().summarized(), + writer::Basic::stdout().summarized(), ) } } @@ -931,15 +919,15 @@ where /// `@serial` [tag] is present on a [`Scenario`]; /// * Allowed to run up to 64 [`Concurrent`] [`Scenario`]s. /// - /// * [`Writer`] — [`Normalized`] and [`Summarized`] [`writer::Basic`]. + /// * [`Writer`] — [`Normalize`] and [`Summarize`] [`writer::Basic`]. /// /// [`Concurrent`]: runner::basic::ScenarioType::Concurrent - /// [`Normalized`]: writer::Normalized + /// [`Normalize`]: writer::Normalize /// [`Parser`]: parser::Parser /// [`Scenario`]: gherkin::Scenario /// [`Serial`]: runner::basic::ScenarioType::Serial /// [`ScenarioType`]: runner::basic::ScenarioType - /// [`Summarized`]: writer::Summarized + /// [`Summarize`]: writer::Summarize /// /// [tag]: https://cucumber.io/docs/cucumber/api/#tags #[must_use] @@ -1174,7 +1162,7 @@ where W: World, P: Parser, R: Runner, - Wr: FailureWriter, + Wr: writer::Failure + writer::Normalized, Cli: StructOpt + StructOptInternal, { /// Runs [`Cucumber`]. diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 1c5f0983..483a30c7 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -40,23 +40,20 @@ pub use self::basic::{Basic, ScenarioType}; /// some [`Scenario`], it should be resolved as [`ScenarioType::Serial`] by the /// [`Runner`]. /// +/// Because of that, [`Writer`], accepting events produced by a [`Runner`] has +/// to be [`Normalized`]. +/// /// All those rules are considered in a [`Basic`] reference [`Runner`] /// implementation. /// -/// Note, that those rules are recommended in case you are using a -/// [`writer::Normalized`]. Strictly speaking, no one is stopping you from -/// implementing [`Runner`] which sources events completely out-of-order or even -/// skips some of them. For example, this can be useful if you care only about -/// failed [`Step`]s. -/// /// [`Cucumber`]: event::Cucumber /// [`Feature`]: gherkin::Feature +/// [`Normalized`]: crate::writer::Normalized /// [`Parser`]: crate::Parser /// [`Rule`]: gherkin::Rule /// [`Scenario`]: gherkin::Scenario /// [`Step`]: gherkin::Step /// [`Writer`]: crate::Writer -/// [`writer::Normalized`]: crate::writer::Normalized /// /// [happened-before]: https://en.wikipedia.org/wiki/Happened-before pub trait Runner { diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 2cd5402f..6fe1e8e5 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -27,8 +27,12 @@ use structopt::StructOpt; use crate::{ event::{self, Info}, parser, - writer::out::{Styles, WriteStrExt as _}, - ArbitraryWriter, Event, World, Writer, + writer::{ + self, + out::{Styles, WriteStrExt as _}, + Ext as _, + }, + Event, World, Writer, }; // Workaround for overwritten doc-comments. @@ -85,14 +89,13 @@ impl FromStr for Coloring { /// /// # Ordering /// -/// It naively and immediately outputs anything it receives from a [`Runner`], -/// so in case the later executes [`Scenario`]s concurrently, the output will be -/// mixed and unordered. To output in a readable order, consider to wrap this -/// [`Basic`] [`Writer`] into a [`writer::Normalized`]. +/// This [`Writer`] isn't [`Normalized`] by itself, so should be wrapped into +/// a [`writer::Normalize`], otherwise will produce output [`Event`]s in a +/// broken order. /// +/// [`Normalized`]: writer::Normalized /// [`Runner`]: crate::runner::Runner /// [`Scenario`]: gherkin::Scenario -/// [`writer::Normalized`]: crate::writer::Normalized #[derive(Debug, Deref, DerefMut)] pub struct Basic { /// [`io::Write`] implementor to write the output into. @@ -149,7 +152,7 @@ where } #[async_trait(?Send)] -impl<'val, W, Val, Out> ArbitraryWriter<'val, W, Val> for Basic +impl<'val, W, Val, Out> writer::Arbitrary<'val, W, Val> for Basic where W: World + Debug, Val: AsRef + 'val, @@ -165,16 +168,43 @@ where } } -impl Default for Basic { - fn default() -> Self { +impl writer::NonTransforming for Basic {} + +impl Basic { + /// Creates a new [`Normalized`] [`Basic`] [`Writer`] outputting to + /// [`io::Stdout`]. + /// + /// [`Normalized`]: writer::Normalized + #[must_use] + pub fn stdout() -> writer::Normalize { Self::new(io::stdout(), Coloring::Auto, false) } } impl Basic { - /// Creates a new [`Basic`] [`Writer`]. + /// Creates a new [`Normalized`] [`Basic`] [`Writer`] outputting to the + /// given `output`. + /// + /// [`Normalized`]: writer::Normalized + #[must_use] + pub fn new( + output: Out, + color: Coloring, + verbose: bool, + ) -> writer::Normalize { + Self::raw(output, color, verbose).normalized() + } + + /// Creates a new non-[`Normalized`] [`Basic`] [`Writer`] outputting to the + /// given `output`. + /// + /// Use it only if you know what you're doing. Otherwise, consider using + /// [`Basic::new()`] which creates an already [`Normalized`] version of a + /// [`Basic`] [`Writer`]. + /// + /// [`Normalized`]: writer::Normalized #[must_use] - pub fn new(output: Out, color: Coloring, verbose: bool) -> Self { + pub fn raw(output: Out, color: Coloring, verbose: bool) -> Self { let mut basic = Self { output, styles: Styles::new(), diff --git a/src/writer/discard.rs b/src/writer/discard.rs index 133dd06e..96256a17 100644 --- a/src/writer/discard.rs +++ b/src/writer/discard.rs @@ -13,16 +13,14 @@ use async_trait::async_trait; use derive_more::{Deref, DerefMut}; -use crate::{ - event::Cucumber, ArbitraryWriter, Event, FailureWriter, World, Writer, -}; +use crate::{event::Cucumber, writer, Event, World, Writer}; /// Wrapper providing a no-op [`ArbitraryWriter`] implementation. /// /// Intended to be used for feeding a non-[`ArbitraryWriter`] [`Writer`] into a /// [`writer::Tee`], as the later accepts only [`ArbitraryWriter`]s. /// -/// [`writer::Tee`]: crate::writer::Tee +/// [`ArbitraryWriter`]: writer::Arbitrary #[derive(Clone, Copy, Debug, Deref, DerefMut)] pub struct Arbitrary(Wr); @@ -40,8 +38,11 @@ impl + ?Sized> Writer for Arbitrary { } #[async_trait(?Send)] -impl<'val, W: World, Val: 'val, Wr: Writer + ?Sized> - ArbitraryWriter<'val, W, Val> for Arbitrary +impl<'val, W, Val, Wr> writer::Arbitrary<'val, W, Val> for Arbitrary +where + W: World, + Val: 'val, + Wr: Writer + ?Sized, { /// Does nothing. async fn write(&mut self, _: Val) @@ -52,7 +53,7 @@ impl<'val, W: World, Val: 'val, Wr: Writer + ?Sized> } } -impl + ?Sized> FailureWriter +impl + ?Sized> writer::Failure for Arbitrary { fn failed_steps(&self) -> usize { @@ -68,6 +69,10 @@ impl + ?Sized> FailureWriter } } +impl writer::Normalized for Arbitrary {} + +impl writer::NonTransforming for Arbitrary {} + impl Arbitrary { /// Wraps the given [`Writer`] into a [`discard::Arbitrary`] one. /// @@ -84,7 +89,7 @@ impl Arbitrary { /// Intended to be used for feeding a non-[`FailureWriter`] [`Writer`] into a /// [`writer::Tee`], as the later accepts only [`FailureWriter`]s. /// -/// [`writer::Tee`]: crate::writer::Tee +/// [`FailureWriter`]: writer::Failure #[derive(Clone, Copy, Debug, Deref, DerefMut)] pub struct Failure(Wr); @@ -102,8 +107,11 @@ impl + ?Sized> Writer for Failure { } #[async_trait(?Send)] -impl<'val, W: World, Val: 'val, Wr: ArbitraryWriter<'val, W, Val> + ?Sized> - ArbitraryWriter<'val, W, Val> for Failure +impl<'val, W, Val, Wr> writer::Arbitrary<'val, W, Val> for Failure +where + W: World, + Val: 'val, + Wr: writer::Arbitrary<'val, W, Val> + ?Sized, { async fn write(&mut self, val: Val) where @@ -113,7 +121,7 @@ impl<'val, W: World, Val: 'val, Wr: ArbitraryWriter<'val, W, Val> + ?Sized> } } -impl + ?Sized> FailureWriter for Failure { +impl + ?Sized> writer::Failure for Failure { /// Always returns `0`. fn failed_steps(&self) -> usize { 0 @@ -130,6 +138,10 @@ impl + ?Sized> FailureWriter for Failure { } } +impl writer::Normalized for Failure {} + +impl writer::NonTransforming for Failure {} + impl Failure { /// Wraps the given [`Writer`] into a [`discard::Failure`] one. /// diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index 02fa970f..049db49d 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -19,9 +19,7 @@ use std::sync::Arc; use async_trait::async_trait; use derive_more::Deref; -use crate::{ - event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, -}; +use crate::{event, parser, writer, Event, World, Writer}; /// [`Writer`]-wrapper for transforming [`Skipped`] [`Step`]s into [`Failed`]. /// @@ -59,7 +57,7 @@ where Option<&gherkin::Rule>, &gherkin::Scenario, ) -> bool, - Wr: for<'val> ArbitraryWriter<'val, W, String>, + Wr: for<'val> writer::Arbitrary<'val, W, String>, { type Cli = Wr::Cli; @@ -106,11 +104,12 @@ where } #[async_trait(?Send)] -impl<'val, W, Wr, Val, F> ArbitraryWriter<'val, W, Val> for FailOnSkipped +impl<'val, W, Wr, Val, F> writer::Arbitrary<'val, W, Val> + for FailOnSkipped where W: World, Self: Writer, - Wr: ArbitraryWriter<'val, W, Val>, + Wr: writer::Arbitrary<'val, W, Val>, Val: 'val, { async fn write(&mut self, val: Val) @@ -121,9 +120,9 @@ where } } -impl FailureWriter for FailOnSkipped +impl writer::Failure for FailOnSkipped where - Wr: FailureWriter, + Wr: writer::Failure, Self: Writer, { fn failed_steps(&self) -> usize { @@ -139,6 +138,8 @@ where } } +impl writer::Normalized for FailOnSkipped {} + impl From for FailOnSkipped { fn from(writer: Writer) -> Self { Self { diff --git a/src/writer/json.rs b/src/writer/json.rs index 29c47cff..dc973673 100644 --- a/src/writer/json.rs +++ b/src/writer/json.rs @@ -22,17 +22,21 @@ use crate::{ cli, event, feature::ExpandExamplesError, parser, - writer::{self, basic::coerce_error, discard}, - Event, World, Writer, WriterExt as _, + writer::{self, basic::coerce_error, discard, Ext as _}, + Event, World, Writer, }; /// [Cucumber JSON format][1] [`Writer`] implementation outputting JSON to an /// [`io::Write`] implementor. /// -/// Should be wrapped into [`writer::Normalized`] to work correctly, otherwise -/// will panic in runtime as won't be able to form [correct JSON][1]. +/// # Ordering +/// +/// This [`Writer`] isn't [`Normalized`] by itself, so should be wrapped into +/// a [`writer::Normalize`], otherwise will panic in runtime as won't be able to +/// form [correct JSON][1]. /// /// [1]: https://github.com/cucumber/cucumber-json-schema +/// [`Normalized`]: writer::Normalized #[derive(Clone, Debug)] pub struct Json { /// [`io::Write`] implementor to output [JSON][1] into. @@ -65,28 +69,24 @@ impl Writer for Json { } } +impl writer::NonTransforming for Json {} + impl Json { - /// Creates a new normalized [`Json`] [`Writer`] outputting [JSON][1] into - /// the given `output`. + /// Creates a new [`Normalized`] [`Json`] [`Writer`] outputting [JSON][1] + /// into the given `output`. /// + /// [`Normalized`]: writer::Normalized /// [1]: https://github.com/cucumber/cucumber-json-schema #[must_use] - pub fn new(output: Out) -> writer::Normalized { + pub fn new(output: Out) -> writer::Normalize { Self::raw(output).normalized() } - /// Creates a new unnormalized [`Json`] [`Writer`] outputting [JSON][1] into - /// the given `output`, and suitable for feeding into [`tee()`]. - /// - /// # Warning - /// - /// It may panic in runtime as won't be able to form [correct JSON][1] from - /// unordered [`Cucumber` events][2], until is [`normalized()`]. + /// Creates a new non-[`Normalized`] [`Json`] [`Writer`] outputting + /// [JSON][1] into the given `output`, and suitable for feeding into + /// [`tee()`]. /// - /// So, either make it [`normalized()`] before feeding into [`tee()`], or - /// make the whole [`tee()`] pipeline [`normalized()`]. - /// - /// [`normalized()`]: crate::WriterExt::normalized + /// [`Normalized`]: writer::Normalized /// [`tee()`]: crate::WriterExt::tee /// [1]: https://github.com/cucumber/cucumber-json-schema /// [2]: crate::event::Cucumber @@ -97,14 +97,9 @@ impl Json { .discard_arbitrary_writes() } - /// Creates a new raw and unnormalized [`Json`] [`Writer`] outputting + /// Creates a new raw and non-[`Normalized`] [`Json`] [`Writer`] outputting /// [JSON][1] into the given `output`. /// - /// # Warning - /// - /// It may panic in runtime as won't be able to form [correct JSON][1] from - /// unordered [`Cucumber` events][2]. - /// /// Use it only if you know what you're doing. Otherwise, consider using /// [`Json::new()`] which creates an already [`Normalized`] version of /// [`Json`] [`Writer`]. diff --git a/src/writer/junit.rs b/src/writer/junit.rs index e409a8d8..3f46f6c0 100644 --- a/src/writer/junit.rs +++ b/src/writer/junit.rs @@ -34,16 +34,18 @@ use crate::{ /// Advice phrase to use in panic messages of incorrect [events][1] ordering. /// /// [1]: event::Scenario -const WRAP_ADVICE: &str = - "Consider wrapping `Writer` into `writer::Normalized`"; +const WRAP_ADVICE: &str = "Consider wrapping `Writer` into `writer::Normalize`"; /// [JUnit XML report][1] [`Writer`] implementation outputting XML to an /// [`io::Write`] implementor. /// -/// Should be wrapped into [`writer::Normalized`] to work correctly, otherwise -/// will panic in runtime as won't be able to form correct -/// [JUnit `testsuite`s][1]. +/// # Ordering /// +/// This [`Writer`] isn't [`Normalized`] by itself, so should be wrapped into +/// a [`writer::Normalize`], otherwise will panic in runtime as won't be able to +/// form correct [JUnit `testsuite`s][1]. +/// +/// [`Normalized`]: writer::Normalized /// [1]: https://llg.cubic.org/docs/junit #[derive(Debug)] pub struct JUnit { @@ -133,30 +135,22 @@ where } } +impl writer::NonTransforming for JUnit {} + impl JUnit { - /// Creates a new normalized [`JUnit`] [`Writer`] outputting XML report into - /// the given `output`. + /// Creates a new [`Normalized`] [`JUnit`] [`Writer`] outputting XML report + /// into the given `output`. + /// + /// [`Normalized`]: writer::Normalized #[must_use] - pub fn new(output: Out) -> writer::Normalized - where - W: World, - { + pub fn new(output: Out) -> writer::Normalize { Self::raw(output).normalized() } - /// Creates a new unnormalized [`JUnit`] [`Writer`] outputting XML report - /// into the given `output`, and suitable for feeding into [`tee()`]. - /// - /// # Warning + /// Creates a new non-[`Normalized`] [`JUnit`] [`Writer`] outputting XML + /// report into the given `output`, and suitable for feeding into [`tee()`]. /// - /// It may panic in runtime as won't be able to correct - /// [JUnit `testsuite`s][1] from unordered [`Cucumber` events][2], until is - /// [`normalized()`]. - /// - /// So, either make it [`normalized()`] before feeding into [`tee()`], or - /// make the whole [`tee()`] pipeline [`normalized()`]. - /// - /// [`normalized()`]: crate::WriterExt::normalized + /// [`Normalized`]: writer::Normalized /// [`tee()`]: crate::WriterExt::tee /// [1]: https://llg.cubic.org/docs/junit /// [2]: crate::event::Cucumber @@ -167,13 +161,8 @@ impl JUnit { .discard_arbitrary_writes() } - /// Creates a new raw and unnormalized [`JUnit`] [`Writer`] outputting XML - /// report into the given `output`. - /// - /// # Warning - /// - /// It may panic in runtime as won't be able to form correct - /// [JUnit `testsuite`s][1] from unordered [`Cucumber` events][2]. + /// Creates a new raw and non-[`Normalized`] [`JUnit`] [`Writer`] outputting + /// XML report into the given `output`. /// /// Use it only if you know what you're doing. Otherwise, consider using /// [`JUnit::new()`] which creates an already [`Normalized`] version of @@ -356,7 +345,9 @@ impl JUnit { } }; - let mut basic_wr = writer::Basic::new( + // We should be passing normalized events here, + // so using `writer::Basic::raw()` is OK. + let mut basic_wr = writer::Basic::raw( WritableString(String::new()), Coloring::Never, false, diff --git a/src/writer/mod.rs b/src/writer/mod.rs index a4f77447..80b1e202 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -19,10 +19,10 @@ pub mod fail_on_skipped; pub mod json; #[cfg(feature = "output-junit")] pub mod junit; -pub mod normalized; +pub mod normalize; pub mod out; pub mod repeat; -pub mod summarized; +pub mod summarize; pub mod tee; use async_trait::async_trait; @@ -39,19 +39,29 @@ pub use self::json::Json; pub use self::junit::JUnit; #[doc(inline)] pub use self::{ - basic::Basic, fail_on_skipped::FailOnSkipped, normalized::Normalized, - repeat::Repeat, summarized::Summarized, tee::Tee, + basic::{Basic, Coloring}, + fail_on_skipped::FailOnSkipped, + normalize::{Normalize, Normalized}, + repeat::Repeat, + summarize::{Summarizable, Summarize}, + tee::Tee, }; /// Writer of [`Cucumber`] events to some output. /// +/// As [`Runner`] produces events in a [happened-before] order (see +/// [its order guarantees][1]), [`Writer`]s are required to be [`Normalized`]. +/// /// As [`Cucumber::run()`] returns [`Writer`], it can hold some state inside for -/// inspection after execution. See [`Summarized`] and +/// inspection after execution. See [`Summarize`] and /// [`Cucumber::run_and_exit()`] for examples. /// /// [`Cucumber`]: crate::event::Cucumber /// [`Cucumber::run()`]: crate::Cucumber::run /// [`Cucumber::run_and_exit()`]: crate::Cucumber::run_and_exit +/// [`Runner`]: crate::Runner +/// [1]: crate::Runner#order-guarantees +/// [happened-before]: https://en.wikipedia.org/wiki/Happened-before #[async_trait(?Send)] pub trait Writer { /// CLI options of this [`Writer`]. In case no options should be introduced, @@ -123,17 +133,17 @@ pub trait Failure: Writer { /// Extension of [`Writer`] allowing its normalization and summarization. #[sealed] pub trait Ext: Sized { - /// Wraps this [`Writer`] into a [`Normalized`] version. + /// Wraps this [`Writer`] into a [`Normalize`]d version. /// - /// See [`Normalized`] for more information. + /// See [`Normalize`] for more information. #[must_use] - fn normalized(self) -> Normalized; + fn normalized(self) -> Normalize; /// Wraps this [`Writer`] to print a summary at the end of an output. /// - /// See [`Summarized`] for more information. + /// See [`Summarize`] for more information. #[must_use] - fn summarized(self) -> Summarized; + fn summarized(self) -> Summarize; /// Wraps this [`Writer`] to fail on [`Skipped`] [`Step`]s if their /// [`Scenario`] isn't marked with `@allow_skipped` tag. @@ -217,12 +227,12 @@ pub trait Ext: Sized { #[sealed] impl Ext for T { - fn normalized(self) -> Normalized { - Normalized::new(self) + fn normalized(self) -> Normalize { + Normalize::new(self) } - fn summarized(self) -> Summarized { - Summarized::from(self) + fn summarized(self) -> Summarize { + Summarize::from(self) } fn fail_on_skipped(self) -> FailOnSkipped { @@ -267,3 +277,127 @@ impl Ext for T { discard::Failure::wrap(self) } } + +/// Marker indicating that a [`Writer`] doesn't transform or rearrange events. +/// +/// It's used to ensure that a [`Writer`]s pipeline is built in the right order, +/// avoiding situations like an event transformation isn't done before it's +/// [`Repeat`]ed. +/// +/// # Example +/// +/// If you want to pipeline [`FailOnSkipped`], [`Summarize`] and [`Repeat`] +/// [`Writer`]s, the code won't compile because of the wrong pipelining order. +/// +/// ```rust,compile_fail +/// # use std::convert::Infallible; +/// # +/// # use async_trait::async_trait; +/// # use cucumber::{writer, WorldInit, WriterExt as _}; +/// # +/// # #[derive(Debug, WorldInit)] +/// # struct MyWorld; +/// # +/// # #[async_trait(?Send)] +/// # impl cucumber::World for MyWorld { +/// # type Error = Infallible; +/// # +/// # async fn new() -> Result { +/// # Ok(Self) +/// # } +/// # } +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// MyWorld::cucumber() +/// .with_writer( +/// // `Writer`s pipeline is constructed in a reversed order. +/// writer::Basic::stdout() +/// .fail_on_skipped() // Fails as `Repeat` will re-output skipped +/// .repeat_failed() // steps instead of failed ones. +/// .summarized() +/// ) +/// .run_and_exit("tests/features/readme") +/// .await; +/// # } +/// ``` +/// +/// ```rust,compile_fail +/// # use std::convert::Infallible; +/// # +/// # use async_trait::async_trait; +/// # use cucumber::{writer, WorldInit, WriterExt as _}; +/// # +/// # #[derive(Debug, WorldInit)] +/// # struct MyWorld; +/// # +/// # #[async_trait(?Send)] +/// # impl cucumber::World for MyWorld { +/// # type Error = Infallible; +/// # +/// # async fn new() -> Result { +/// # Ok(Self) +/// # } +/// # } +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// MyWorld::cucumber() +/// .with_writer( +/// // `Writer`s pipeline is constructed in a reversed order. +/// writer::Basic::stdout() +/// .repeat_failed() +/// .fail_on_skipped() // Fails as `Summarize` will count skipped +/// .summarized() // steps instead of `failed` ones. +/// ) +/// .run_and_exit("tests/features/readme") +/// .await; +/// # } +/// ``` +/// +/// ```rust +/// # use std::{convert::Infallible, panic::AssertUnwindSafe}; +/// # +/// # use async_trait::async_trait; +/// # use cucumber::{writer, WorldInit, WriterExt as _}; +/// # use futures::FutureExt as _; +/// # +/// # #[derive(Debug, WorldInit)] +/// # struct MyWorld; +/// # +/// # #[async_trait(?Send)] +/// # impl cucumber::World for MyWorld { +/// # type Error = Infallible; +/// # +/// # async fn new() -> Result { +/// # Ok(Self) +/// # } +/// # } +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// # let fut = async { +/// MyWorld::cucumber() +/// .with_writer( +/// // `Writer`s pipeline is constructed in a reversed order. +/// writer::Basic::stdout() // And, finally, print them. +/// .repeat_failed() // Then, repeat failed ones once again. +/// .summarized() // Only then, count summary for them. +/// .fail_on_skipped(), // First, transform skipped steps to failed. +/// ) +/// .run_and_exit("tests/features/readme") +/// .await; +/// # }; +/// # let err = AssertUnwindSafe(fut) +/// # .catch_unwind() +/// # .await +/// # .expect_err("should err"); +/// # let err = err.downcast_ref::().unwrap(); +/// # assert_eq!(err, "1 step failed"); +/// # } +/// ``` +/// +/// [`Failed`]: event::Step::Failed +/// [`FailOnSkipped`]: crate::writer::FailOnSkipped +/// [`Skipped`]: event::Step::Skipped +pub trait NonTransforming {} diff --git a/src/writer/normalized.rs b/src/writer/normalize.rs similarity index 94% rename from src/writer/normalized.rs rename to src/writer/normalize.rs index cbe6ae00..fcbf5f0f 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalize.rs @@ -19,11 +19,12 @@ use linked_hash_map::LinkedHashMap; use crate::{ event::{self, Metadata}, - parser, ArbitraryWriter, Event, FailureWriter, Writer, + parser, writer, Event, Writer, }; /// Wrapper for a [`Writer`] implementation for outputting events corresponding -/// to _order guarantees_ from the [`Runner`] in a normalized readable order. +/// to _order guarantees_ from the [`Runner`] in a [`Normalized`] readable +/// order. /// /// Doesn't output anything by itself, but rather is used as a combinator for /// rearranging events and feeding them to the underlying [`Writer`]. @@ -40,7 +41,7 @@ use crate::{ /// [`Scenario`]: gherkin::Scenario /// [`Step`]: gherkin::Step #[derive(Debug, Deref)] -pub struct Normalized { +pub struct Normalize { /// Original [`Writer`] to normalize output of. #[deref] pub writer: Writer, @@ -49,7 +50,7 @@ pub struct Normalized { queue: CucumberQueue, } -impl Normalized { +impl Normalize { /// Creates a new [`Normalized`] wrapper, which will rearrange [`event`]s /// and feed them to the given [`Writer`]. #[must_use] @@ -62,7 +63,7 @@ impl Normalized { } #[async_trait(?Send)] -impl> Writer for Normalized { +impl> Writer for Normalize { type Cli = Wr::Cli; async fn handle_event( @@ -131,9 +132,9 @@ impl> Writer for Normalized { } #[async_trait(?Send)] -impl<'val, W, Wr, Val> ArbitraryWriter<'val, W, Val> for Normalized +impl<'val, W, Wr, Val> writer::Arbitrary<'val, W, Val> for Normalize where - Wr: ArbitraryWriter<'val, W, Val>, + Wr: writer::Arbitrary<'val, W, Val>, Val: 'val, { async fn write(&mut self, val: Val) @@ -144,9 +145,9 @@ where } } -impl FailureWriter for Normalized +impl writer::Failure for Normalize where - Wr: FailureWriter, + Wr: writer::Failure, Self: Writer, { fn failed_steps(&self) -> usize { @@ -162,6 +163,32 @@ where } } +impl writer::NonTransforming + for Normalize +{ +} + +/// Marker indicating that a [`Writer`] can accept events in a [happened-before] +/// order. +/// +/// This means one of two things: +/// +/// 1. Either [`Writer`] doesn't depend on events ordering. +/// For example, [`Writer`] which prints only [`Failed`] [`Step`]s. +/// +/// 2. Or [`Writer`] does depend on events ordering, but implements some logic +/// to rearrange them. +/// For example, a [`Normalize`] wrapper will rearrange events and pass them +/// to the underlying [`Writer`], like a [`Runner`] wasn't concurrent at all. +/// +/// [`Step`]: gherkin::Step +/// [`Failed`]: event::Step::Failed +/// [`Runner`]: crate::Runner +/// [happened-before]: https://en.wikipedia.org/wiki/Happened-before +pub trait Normalized {} + +impl Normalized for Normalize {} + /// Normalization queue for incoming events. /// /// We use [`LinkedHashMap`] everywhere throughout this module to ensure FIFO diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 6ef01ff2..2d438c39 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -15,17 +15,23 @@ use std::mem; use async_trait::async_trait; use derive_more::Deref; -use crate::{ - event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, -}; +use crate::{event, parser, writer, Event, World, Writer}; + +/// Alias for a [`fn`] predicate deciding whether an event should be +/// re-outputted or not. +pub type FilterEvent = + fn(&parser::Result>>) -> bool; /// Wrapper for a [`Writer`] implementation for re-outputting events at the end /// of an output, based on a filter predicated. /// /// Useful for re-outputting [skipped] or [failed] [`Step`]s. /// +/// An underlying [`Writer`] has to be [`NonTransforming`]. +/// /// [failed]: crate::WriterExt::repeat_failed /// [skipped]: crate::WriterExt::repeat_skipped +/// [`NonTransforming`]: writer::NonTransforming /// [`Step`]: gherkin::Step #[derive(Debug, Deref)] pub struct Repeat> { @@ -40,16 +46,11 @@ pub struct Repeat> { events: Vec>>>, } -/// Alias for a [`fn`] predicate deciding whether an event should be -/// re-outputted or not. -pub type FilterEvent = - fn(&parser::Result>>) -> bool; - #[async_trait(?Send)] impl Writer for Repeat where W: World, - Wr: Writer, + Wr: Writer + writer::NonTransforming, F: Fn(&parser::Result>>) -> bool, { type Cli = Wr::Cli; @@ -77,10 +78,10 @@ where } #[async_trait(?Send)] -impl<'val, W, Wr, Val, F> ArbitraryWriter<'val, W, Val> for Repeat +impl<'val, W, Wr, Val, F> writer::Arbitrary<'val, W, Val> for Repeat where W: World, - Wr: ArbitraryWriter<'val, W, Val>, + Wr: writer::Arbitrary<'val, W, Val> + writer::NonTransforming, Val: 'val, F: Fn(&parser::Result>>) -> bool, { @@ -92,9 +93,9 @@ where } } -impl FailureWriter for Repeat +impl writer::Failure for Repeat where - Wr: FailureWriter, + Wr: writer::Failure + writer::NonTransforming, Self: Writer, { fn failed_steps(&self) -> usize { @@ -110,6 +111,10 @@ where } } +impl writer::Normalized for Repeat {} + +impl writer::Summarizable for Repeat {} + impl Repeat { /// Creates a new [`Writer`] for re-outputting events at the end of an /// output in case the given `filter` predicated returns `true`. diff --git a/src/writer/summarized.rs b/src/writer/summarize.rs similarity index 67% rename from src/writer/summarized.rs rename to src/writer/summarize.rs index f5400db3..d2c813de 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarize.rs @@ -17,8 +17,9 @@ use derive_more::Deref; use itertools::Itertools as _; use crate::{ - event, parser, writer::out::Styles, ArbitraryWriter, Event, FailureWriter, - World, Writer, + event, parser, + writer::{self, out::Styles}, + Event, World, Writer, }; /// Execution statistics. @@ -85,16 +86,37 @@ enum Indicator { Skipped, } +/// Possible states of a [`Summarize`] [`Writer`]. +#[derive(Clone, Copy, Debug)] +enum State { + /// [`Finished`] event hasn't been encountered yet. + /// + /// [`Finished`]: event::Cucumber::Finished + InProgress, + + /// [`Finished`] event was encountered, but summary hasn't been output yet. + /// + /// [`Finished`]: event::Cucumber::Finished + FinishedButNotOutput, + + /// [`Finished`] event was encountered and summary was output. + /// + /// [`Finished`]: event::Cucumber::Finished + FinishedAndOutput, +} + /// Wrapper for a [`Writer`] for outputting an execution summary (number of /// executed features, scenarios, steps and parsing errors). /// -/// __Note:__ The underlying [`Writer`] is expected to be an [`ArbitraryWriter`] +/// Underlying [`Writer`] has to be [`Summarizable`] and [`ArbitraryWriter`] /// with `Value` accepting [`String`]. If your underlying [`ArbitraryWriter`] /// operates with something like JSON (or any other type), you should implement -/// a [`Writer`] on [`Summarized`] by yourself, to provide the required summary +/// a [`Writer`] on [`Summarize`] by yourself, to provide the required summary /// format. +/// +/// [`ArbitraryWriter`]: writer::Arbitrary #[derive(Debug, Deref)] -pub struct Summarized { +pub struct Summarize { /// Original [`Writer`] to summarize output of. #[deref] pub writer: Writer, @@ -129,6 +151,9 @@ pub struct Summarized { /// [`Scenario`]: gherkin::Scenario pub failed_hooks: usize, + /// Current [`State`] of this [`Writer`]. + state: State, + /// Handled [`Scenario`]s to collect [`Stats`]. /// /// [`Scenario`]: gherkin::Scenario @@ -136,10 +161,10 @@ pub struct Summarized { } #[async_trait(?Send)] -impl Writer for Summarized +impl Writer for Summarize where W: World, - Wr: for<'val> ArbitraryWriter<'val, W, String>, + Wr: for<'val> writer::Arbitrary<'val, W, String> + Summarizable, { type Cli = Wr::Cli; @@ -150,38 +175,46 @@ where ) { use event::{Cucumber, Feature, Rule}; - let mut finished = false; - match ev.as_deref() { - Err(_) => self.parsing_errors += 1, - Ok(Cucumber::Feature(_, ev)) => match ev { - Feature::Started => self.features += 1, - Feature::Rule(_, Rule::Started) => { - self.rules += 1; - } - Feature::Rule(_, Rule::Scenario(sc, ev)) - | Feature::Scenario(sc, ev) => { - self.handle_scenario(sc, ev); + // Once `Cucumber::Finished` is emitted, we just pass events through, + // without collecting `Stats`. + // This is done to avoid miscalculations if this `Writer` happens to be + // wrapped by a `writer::Repeat` or similar. + if let State::InProgress = self.state { + match ev.as_deref() { + Err(_) => self.parsing_errors += 1, + Ok(Cucumber::Feature(_, ev)) => match ev { + Feature::Started => self.features += 1, + Feature::Rule(_, Rule::Started) => { + self.rules += 1; + } + Feature::Rule(_, Rule::Scenario(sc, ev)) + | Feature::Scenario(sc, ev) => { + self.handle_scenario(sc, ev); + } + Feature::Finished | Feature::Rule(..) => {} + }, + Ok(Cucumber::Finished) => { + self.state = State::FinishedButNotOutput; } - Feature::Finished | Feature::Rule(..) => {} - }, - Ok(Cucumber::Finished) => finished = true, - Ok(Cucumber::Started) => {} - }; + Ok(Cucumber::Started) => {} + }; + } self.writer.handle_event(ev, cli).await; - if finished { + if let State::FinishedButNotOutput = self.state { + self.state = State::FinishedAndOutput; self.writer.write(Styles::new().summary(self)).await; } } } #[async_trait(?Send)] -impl<'val, W, Wr, Val> ArbitraryWriter<'val, W, Val> for Summarized +impl<'val, W, Wr, Val> writer::Arbitrary<'val, W, Val> for Summarize where W: World, Self: Writer, - Wr: ArbitraryWriter<'val, W, Val>, + Wr: writer::Arbitrary<'val, W, Val>, Val: 'val, { async fn write(&mut self, val: Val) @@ -192,7 +225,7 @@ where } } -impl FailureWriter for Summarized +impl writer::Failure for Summarize where W: World, Self: Writer, @@ -210,7 +243,11 @@ where } } -impl From for Summarized { +impl writer::Normalized for Summarize {} + +impl writer::NonTransforming for Summarize {} + +impl From for Summarize { fn from(writer: Writer) -> Self { Self { writer, @@ -228,12 +265,13 @@ impl From for Summarized { }, parsing_errors: 0, failed_hooks: 0, + state: State::InProgress, handled_scenarios: HashMap::new(), } } } -impl Summarized { +impl Summarize { /// Keeps track of [`Step`]'s [`Stats`]. /// /// [`Step`]: gherkin::Step @@ -313,20 +351,105 @@ impl Summarized { } } -impl Summarized { - /// Wraps the given [`Writer`] into a new [`Summarized`] one. +impl Summarize { + /// Wraps the given [`Writer`] into a new [`Summarize`]d one. #[must_use] pub fn new(writer: Writer) -> Self { Self::from(writer) } } +/// Marker indicating that a [`Writer`] can be wrapped into a [`Summarize`]. +/// +/// Not any [`Writer`] can be wrapped into a [`Summarize`], as it may transform +/// events inside and the summary won't reflect outputted events correctly. +/// +/// So, this trait ensures that a wrong [`Writer`]s pipeline cannot be build. +/// +/// # Example +/// +/// ```rust,compile_fail +/// # use std::convert::Infallible; +/// # +/// # use async_trait::async_trait; +/// # use cucumber::{WorldInit, writer, WriterExt as _}; +/// # +/// # #[derive(Debug, WorldInit)] +/// # struct MyWorld; +/// # +/// # #[async_trait(?Send)] +/// # impl cucumber::World for MyWorld { +/// # type Error = Infallible; +/// # +/// # async fn new() -> Result { +/// # Ok(Self) +/// # } +/// # } +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// MyWorld::cucumber() +/// .with_writer( +/// // `Writer`s pipeline is constructed in a reversed order. +/// writer::Basic::stdout() +/// .fail_on_skipped() // Fails as `Summarize` will count skipped +/// .summarized() // steps instead of failed. +/// ) +/// .run_and_exit("tests/features/readme") +/// .await; +/// # } +/// ``` +/// +/// ```rust +/// # use std::{convert::Infallible, panic::AssertUnwindSafe}; +/// # +/// # use async_trait::async_trait; +/// # use cucumber::{WorldInit, writer, WriterExt as _}; +/// # use futures::FutureExt as _; +/// # +/// # #[derive(Debug, WorldInit)] +/// # struct MyWorld; +/// # +/// # #[async_trait(?Send)] +/// # impl cucumber::World for MyWorld { +/// # type Error = Infallible; +/// # +/// # async fn new() -> Result { +/// # Ok(Self) +/// # } +/// # } +/// # +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// # let fut = async { +/// MyWorld::cucumber() +/// .with_writer( +/// // `Writer`s pipeline is constructed in a reversed order. +/// writer::Basic::stdout() // And, finally, print them. +/// .summarized() // Only then, count summary for them. +/// .fail_on_skipped() // First, transform skipped steps to failed. +/// ) +/// .run_and_exit("tests/features/readme") +/// .await; +/// # }; +/// # let err = AssertUnwindSafe(fut) +/// # .catch_unwind() +/// # .await +/// # .expect_err("should err"); +/// # let err = err.downcast_ref::().unwrap(); +/// # assert_eq!(err, "1 step failed"); +/// # } +/// ``` +pub trait Summarizable {} + +impl Summarizable for T {} + // We better keep this here, as it's related to summarization only. #[allow(clippy::multiple_inherent_impl)] impl Styles { /// Generates a formatted summary [`String`]. #[must_use] - pub fn summary(&self, summary: &Summarized) -> String { + pub fn summary(&self, summary: &Summarize) -> String { let features = self.maybe_plural("feature", summary.features); let rules = (summary.rules > 0) diff --git a/src/writer/tee.rs b/src/writer/tee.rs index c69d4725..498b3030 100644 --- a/src/writer/tee.rs +++ b/src/writer/tee.rs @@ -15,9 +15,7 @@ use std::cmp; use async_trait::async_trait; use futures::future; -use crate::{ - cli, event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, -}; +use crate::{cli, event, parser, writer, Event, World, Writer}; /// Wrapper for passing events to multiple terminating [`Writer`]s /// simultaneously. @@ -31,6 +29,8 @@ use crate::{ /// [`WriterExt::discard_failure_writes()`][2] methods to provide the one with /// no-op implementations. /// +/// [`ArbitraryWriter`]: writer::Arbitrary +/// [`FailureWriter`]: writer::Failure /// [1]: crate::WriterExt::discard_arbitrary_writes /// [2]: crate::WriterExt::discard_failure_writes #[derive(Clone, Copy, Debug)] @@ -74,11 +74,11 @@ where } #[async_trait(?Send)] -impl<'val, W, L, R, Val> ArbitraryWriter<'val, W, Val> for Tee +impl<'val, W, L, R, Val> writer::Arbitrary<'val, W, Val> for Tee where W: World, - L: ArbitraryWriter<'val, W, Val>, - R: ArbitraryWriter<'val, W, Val>, + L: writer::Arbitrary<'val, W, Val>, + R: writer::Arbitrary<'val, W, Val>, Val: Clone + 'val, { async fn write(&mut self, val: Val) @@ -89,10 +89,10 @@ where } } -impl FailureWriter for Tee +impl writer::Failure for Tee where - L: FailureWriter, - R: FailureWriter, + L: writer::Failure, + R: writer::Failure, Self: Writer, { fn failed_steps(&self) -> usize { @@ -110,3 +110,17 @@ where cmp::max(self.left.hook_errors(), self.right.hook_errors()) } } + +impl writer::Normalized for Tee +where + L: writer::Normalized, + R: writer::Normalized, +{ +} + +impl writer::NonTransforming for Tee +where + L: writer::NonTransforming, + R: writer::NonTransforming, +{ +}