cucumber/writer/
mod.rs

1// Copyright (c) 2018-2024  Brendan Molloy <brendan@bbqsrc.net>,
2//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3//                          Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Tools for outputting [`Cucumber`] events.
12//!
13//! [`Cucumber`]: crate::event::Cucumber
14
15pub mod basic;
16pub mod discard;
17pub mod fail_on_skipped;
18#[cfg(feature = "output-json")]
19pub mod json;
20#[cfg(feature = "output-junit")]
21pub mod junit;
22#[cfg(feature = "libtest")]
23pub mod libtest;
24pub mod normalize;
25pub mod or;
26pub mod out;
27pub mod repeat;
28pub mod summarize;
29pub mod tee;
30
31use std::future::Future;
32
33use sealed::sealed;
34
35use crate::{event, parser, Event};
36
37#[cfg(feature = "output-json")]
38#[doc(inline)]
39pub use self::json::Json;
40#[cfg(feature = "output-junit")]
41#[doc(inline)]
42pub use self::junit::JUnit;
43#[cfg(feature = "libtest")]
44#[doc(inline)]
45pub use self::libtest::Libtest;
46#[doc(inline)]
47pub use self::{
48    basic::{Basic, Coloring},
49    fail_on_skipped::FailOnSkipped,
50    normalize::{AssertNormalized, Normalize, Normalized},
51    or::Or,
52    repeat::Repeat,
53    summarize::{Summarizable, Summarize},
54    tee::Tee,
55};
56
57/// Writer of [`Cucumber`] events to some output.
58///
59/// As [`Runner`] produces events in a [happened-before] order (see
60/// [its order guarantees][1]), [`Writer`]s are required to be [`Normalized`].
61///
62/// As [`Cucumber::run()`] returns [`Writer`], it can hold some state inside for
63/// inspection after execution. See [`Summarize`] and
64/// [`Cucumber::run_and_exit()`] for examples.
65///
66/// [`Cucumber`]: crate::event::Cucumber
67/// [`Cucumber::run()`]: crate::Cucumber::run
68/// [`Cucumber::run_and_exit()`]: crate::Cucumber::run_and_exit
69/// [`Runner`]: crate::Runner
70/// [1]: crate::Runner#order-guarantees
71/// [happened-before]: https://en.wikipedia.org/wiki/Happened-before
72pub trait Writer<World> {
73    /// CLI options of this [`Writer`]. In case no options should be introduced,
74    /// just use [`cli::Empty`].
75    ///
76    /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be
77    /// merged together, so overlapping arguments will cause a runtime panic.
78    ///
79    /// [`cli::Empty`]: crate::cli::Empty
80    /// [`Parser`]: crate::Parser
81    /// [`Runner`]: crate::Runner
82    type Cli: clap::Args;
83
84    /// Handles the given [`Cucumber`] event.
85    ///
86    /// [`Cucumber`]: crate::event::Cucumber
87    fn handle_event(
88        &mut self,
89        ev: parser::Result<Event<event::Cucumber<World>>>,
90        cli: &Self::Cli,
91    ) -> impl Future<Output = ()>;
92}
93
94/// [`Writer`] that also can output an arbitrary `Value` in addition to
95/// regular [`Cucumber`] events.
96///
97/// [`Cucumber`]: event::Cucumber
98pub trait Arbitrary<World, Value>: Writer<World> {
99    /// Writes `val` to the [`Writer`]'s output.
100    fn write(&mut self, val: Value) -> impl Future<Output = ()>;
101}
102
103/// [`Writer`] tracking a number of [`Passed`], [`Skipped`], [`Failed`]
104/// [`Step`]s and parsing errors.
105///
106/// [`Failed`]: event::Step::Failed
107/// [`Passed`]: event::Step::Passed
108/// [`Skipped`]: event::Step::Skipped
109/// [`Step`]: gherkin::Step
110pub trait Stats<World>: Writer<World> {
111    /// Returns number of [`Passed`] [`Step`]s.
112    ///
113    /// [`Passed`]: event::Step::Passed
114    /// [`Step`]: gherkin::Step
115    #[must_use]
116    fn passed_steps(&self) -> usize;
117
118    /// Returns number of [`Skipped`] [`Step`]s.
119    ///
120    /// [`Skipped`]: event::Step::Skipped
121    /// [`Step`]: gherkin::Step
122    #[must_use]
123    fn skipped_steps(&self) -> usize;
124
125    /// Returns number of [`Failed`] [`Step`]s.
126    ///
127    /// [`Failed`]: event::Step::Failed
128    /// [`Step`]: gherkin::Step
129    #[must_use]
130    fn failed_steps(&self) -> usize;
131
132    /// Returns number of retried [`Step`]s.
133    ///
134    /// [`Step`]: gherkin::Step
135    #[must_use]
136    fn retried_steps(&self) -> usize;
137
138    /// Returns number of parsing errors.
139    #[must_use]
140    fn parsing_errors(&self) -> usize;
141
142    /// Returns number of failed [`Scenario`] hooks.
143    ///
144    /// [`Scenario`]: gherkin::Scenario
145    #[must_use]
146    fn hook_errors(&self) -> usize;
147
148    /// Indicates whether there were failures/errors during execution.
149    #[must_use]
150    fn execution_has_failed(&self) -> bool {
151        self.failed_steps() > 0
152            || self.parsing_errors() > 0
153            || self.hook_errors() > 0
154    }
155}
156
157/// Extension of [`Writer`] allowing its normalization and summarization.
158#[sealed]
159pub trait Ext: Sized {
160    /// Asserts this [`Writer`] being [`Normalized`].
161    ///
162    /// Technically is no-op, only forcing the [`Writer`] to become
163    /// [`Normalized`] despite it actually doesn't represent the one.
164    ///
165    /// If you need a real normalization, use [`normalized()`] instead.
166    ///
167    /// > ⚠️ __WARNING__: Should be used only in case you are absolutely sure,
168    /// >                 that incoming events will be emitted in a
169    /// >                 [`Normalized`] order.
170    /// >                 For example, in case [`max_concurrent_scenarios()`][1]
171    /// >                 is set to `1`.
172    ///
173    /// [`normalized()`]: Ext::normalized
174    /// [1]: crate::runner::Basic::max_concurrent_scenarios()
175    #[must_use]
176    fn assert_normalized(self) -> AssertNormalized<Self>;
177
178    /// Wraps this [`Writer`] into a [`Normalize`]d version.
179    ///
180    /// See [`Normalize`] for more information.
181    #[must_use]
182    fn normalized<W>(self) -> Normalize<W, Self>;
183
184    /// Wraps this [`Writer`] to print a summary at the end of an output.
185    ///
186    /// See [`Summarize`] for more information.
187    #[must_use]
188    fn summarized(self) -> Summarize<Self>;
189
190    /// Wraps this [`Writer`] to fail on [`Skipped`] [`Step`]s if their
191    /// [`Scenario`] isn't marked with `@allow.skipped` tag.
192    ///
193    /// See [`FailOnSkipped`] for more information.
194    ///
195    /// [`Scenario`]: gherkin::Scenario
196    /// [`Skipped`]: event::Step::Skipped
197    /// [`Step`]: gherkin::Step
198    #[must_use]
199    fn fail_on_skipped(self) -> FailOnSkipped<Self>;
200
201    /// Wraps this [`Writer`] to fail on [`Skipped`] [`Step`]s if the given
202    /// `with` predicate returns `true`.
203    ///
204    /// See [`FailOnSkipped`] for more information.
205    ///
206    /// [`Scenario`]: gherkin::Scenario
207    /// [`Skipped`]: event::Step::Skipped
208    /// [`Step`]: gherkin::Step
209    #[must_use]
210    fn fail_on_skipped_with<F>(self, with: F) -> FailOnSkipped<Self, F>
211    where
212        F: Fn(
213            &gherkin::Feature,
214            Option<&gherkin::Rule>,
215            &gherkin::Scenario,
216        ) -> bool;
217
218    /// Wraps this [`Writer`] to re-output [`Skipped`] [`Step`]s at the end of
219    /// an output.
220    ///
221    /// [`Skipped`]: event::Step::Skipped
222    /// [`Step`]: gherkin::Step
223    #[must_use]
224    fn repeat_skipped<W>(self) -> Repeat<W, Self>;
225
226    /// Wraps this [`Writer`] to re-output [`Failed`] [`Step`]s or [`Parser`]
227    /// errors at the end of an output.
228    ///
229    /// [`Failed`]: event::Step::Failed
230    /// [`Parser`]: crate::Parser
231    /// [`Step`]: gherkin::Step
232    #[must_use]
233    fn repeat_failed<W>(self) -> Repeat<W, Self>;
234
235    /// Wraps this [`Writer`] to re-output `filter`ed events at the end of an
236    /// output.
237    #[must_use]
238    fn repeat_if<W, F>(self, filter: F) -> Repeat<W, Self, F>
239    where
240        F: Fn(&parser::Result<Event<event::Cucumber<W>>>) -> bool;
241
242    /// Attaches the provided `other` [`Writer`] to the current one for passing
243    /// events to both of them simultaneously.
244    #[must_use]
245    fn tee<W, Wr: Writer<W>>(self, other: Wr) -> Tee<Self, Wr>;
246
247    /// Wraps this [`Writer`] into a [`discard::Arbitrary`] one, providing a
248    /// no-op [`ArbitraryWriter`] implementation.
249    ///
250    /// Intended to be used for feeding a non-[`ArbitraryWriter`] [`Writer`]
251    /// into a [`tee()`], as the later accepts only [`ArbitraryWriter`]s.
252    ///
253    /// [`tee()`]: Ext::tee
254    /// [`ArbitraryWriter`]: Arbitrary
255    #[must_use]
256    fn discard_arbitrary_writes(self) -> discard::Arbitrary<Self>;
257
258    /// Wraps this [`Writer`] into a [`discard::Stats`] one, providing a no-op
259    /// [`StatsWriter`] implementation returning only `0`.
260    ///
261    /// Intended to be used for feeding a non-[`StatsWriter`] [`Writer`] into a
262    /// [`tee()`], as the later accepts only [`StatsWriter`]s.
263    ///
264    /// [`tee()`]: Ext::tee
265    /// [`StatsWriter`]: Stats
266    #[must_use]
267    fn discard_stats_writes(self) -> discard::Stats<Self>;
268}
269
270#[sealed]
271impl<T> Ext for T {
272    fn assert_normalized(self) -> AssertNormalized<Self> {
273        AssertNormalized::new(self)
274    }
275
276    fn normalized<W>(self) -> Normalize<W, Self> {
277        Normalize::new(self)
278    }
279
280    fn summarized(self) -> Summarize<Self> {
281        Summarize::from(self)
282    }
283
284    fn fail_on_skipped(self) -> FailOnSkipped<Self> {
285        FailOnSkipped::from(self)
286    }
287
288    fn fail_on_skipped_with<F>(self, f: F) -> FailOnSkipped<Self, F>
289    where
290        F: Fn(
291            &gherkin::Feature,
292            Option<&gherkin::Rule>,
293            &gherkin::Scenario,
294        ) -> bool,
295    {
296        FailOnSkipped::with(self, f)
297    }
298
299    fn repeat_skipped<W>(self) -> Repeat<W, Self> {
300        Repeat::skipped(self)
301    }
302
303    fn repeat_failed<W>(self) -> Repeat<W, Self> {
304        Repeat::failed(self)
305    }
306
307    fn repeat_if<W, F>(self, filter: F) -> Repeat<W, Self, F>
308    where
309        F: Fn(&parser::Result<Event<event::Cucumber<W>>>) -> bool,
310    {
311        Repeat::new(self, filter)
312    }
313
314    fn tee<W, Wr: Writer<W>>(self, other: Wr) -> Tee<Self, Wr> {
315        Tee::new(self, other)
316    }
317
318    fn discard_arbitrary_writes(self) -> discard::Arbitrary<Self> {
319        discard::Arbitrary::wrap(self)
320    }
321
322    fn discard_stats_writes(self) -> discard::Stats<Self> {
323        discard::Stats::wrap(self)
324    }
325}
326
327/// Marker indicating that a [`Writer`] doesn't transform or rearrange events.
328///
329/// It's used to ensure that a [`Writer`]s pipeline is built in the right order,
330/// avoiding situations like an event transformation isn't done before it's
331/// [`Repeat`]ed.
332///
333/// # Example
334///
335/// If you want to pipeline [`FailOnSkipped`], [`Summarize`] and [`Repeat`]
336/// [`Writer`]s, the code won't compile because of the wrong pipelining order.
337///
338/// ```rust,compile_fail
339/// # use cucumber::{writer, World, WriterExt as _};
340/// #
341/// # #[derive(Debug, Default, World)]
342/// # struct MyWorld;
343/// #
344/// # #[tokio::main(flavor = "current_thread")]
345/// # async fn main() {
346/// MyWorld::cucumber()
347///     .with_writer(
348///         // `Writer`s pipeline is constructed in a reversed order.
349///         writer::Basic::stdout()
350///             .fail_on_skipped() // Fails as `Repeat` will re-output skipped
351///             .repeat_failed()   // steps instead of failed ones.
352///             .summarized()
353///     )
354///     .run_and_exit("tests/features/readme")
355///     .await;
356/// # }
357/// ```
358///
359/// ```rust,compile_fail
360/// # use cucumber::{writer, World, WriterExt as _};
361/// #
362/// # #[derive(Debug, Default, World)]
363/// # struct MyWorld;
364/// #
365/// # #[tokio::main(flavor = "current_thread")]
366/// # async fn main() {
367/// MyWorld::cucumber()
368///     .with_writer(
369///         // `Writer`s pipeline is constructed in a reversed order.
370///         writer::Basic::stdout()
371///             .repeat_failed()
372///             .fail_on_skipped() // Fails as `Summarize` will count skipped
373///             .summarized()      // steps instead of `failed` ones.
374///     )
375///     .run_and_exit("tests/features/readme")
376///     .await;
377/// # }
378/// ```
379///
380/// ```rust
381/// # use std::panic::AssertUnwindSafe;
382/// #
383/// # use cucumber::{writer, World, WriterExt as _};
384/// # use futures::FutureExt as _;
385/// #
386/// # #[derive(Debug, Default, World)]
387/// # struct MyWorld;
388/// #
389/// # #[tokio::main(flavor = "current_thread")]
390/// # async fn main() {
391/// # let fut = async {
392/// MyWorld::cucumber()
393///     .with_writer(
394///         // `Writer`s pipeline is constructed in a reversed order.
395///         writer::Basic::stdout() // And, finally, print them.
396///             .repeat_failed()    // Then, repeat failed ones once again.
397///             .summarized()       // Only then, count summary for them.
398///             .fail_on_skipped(), // First, transform skipped steps to failed.
399///     )
400///     .run_and_exit("tests/features/readme")
401///     .await;
402/// # };
403/// # let err = AssertUnwindSafe(fut)
404/// #     .catch_unwind()
405/// #     .await
406/// #     .expect_err("should err");
407/// # let err = err.downcast_ref::<String>().unwrap();
408/// # assert_eq!(err, "1 step failed");
409/// # }
410/// ```
411///
412/// [`Failed`]: event::Step::Failed
413/// [`Skipped`]: event::Step::Skipped
414pub trait NonTransforming {}
415
416/// Standard verbosity levels of a [`Writer`].
417#[derive(Clone, Copy, Debug, Default)]
418#[repr(u8)]
419pub enum Verbosity {
420    /// None additional info.
421    #[default]
422    Default = 0,
423
424    /// Outputs the whole [`World`] on [`Failed`] [`Step`]s whenever is
425    /// possible.
426    ///
427    /// [`Failed`]: event::Step::Failed
428    /// [`Step`]: gherkin::Step
429    /// [`World`]: crate::World
430    ShowWorld = 1,
431
432    /// Additionally to [`Verbosity::ShowWorld`] outputs [Doc Strings].
433    ///
434    /// [Doc Strings]: https://cucumber.io/docs/gherkin/reference#doc-strings
435    ShowWorldAndDocString = 2,
436}
437
438impl From<u8> for Verbosity {
439    fn from(v: u8) -> Self {
440        match v {
441            0 => Self::Default,
442            1 => Self::ShowWorld,
443            _ => Self::ShowWorldAndDocString,
444        }
445    }
446}
447
448impl From<Verbosity> for u8 {
449    fn from(v: Verbosity) -> Self {
450        match v {
451            Verbosity::Default => 0,
452            Verbosity::ShowWorld => 1,
453            Verbosity::ShowWorldAndDocString => 2,
454        }
455    }
456}
457
458impl Verbosity {
459    /// Indicates whether [`World`] should be outputted on [`Failed`] [`Step`]s
460    /// implying this [`Verbosity`].
461    ///
462    /// [`Failed`]: event::Step::Failed
463    /// [`Step`]: gherkin::Step
464    /// [`World`]: crate::World
465    #[must_use]
466    pub const fn shows_world(&self) -> bool {
467        matches!(self, Self::ShowWorld | Self::ShowWorldAndDocString)
468    }
469
470    /// Indicates whether [`Step::docstring`]s should be outputted implying this
471    /// [`Verbosity`].
472    ///
473    /// [`Step::docstring`]: gherkin::Step::docstring
474    #[must_use]
475    pub const fn shows_docstring(&self) -> bool {
476        matches!(self, Self::ShowWorldAndDocString)
477    }
478}