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}