cucumber/
cucumber.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//! Top-level [Cucumber] executor.
12//!
13//! [Cucumber]: https://cucumber.io
14
15use std::{
16    borrow::Cow,
17    fmt::{self, Debug},
18    marker::PhantomData,
19    mem,
20    path::Path,
21    time::Duration,
22};
23
24use futures::{future::LocalBoxFuture, StreamExt as _};
25use gherkin::tagexpr::TagOperation;
26use regex::Regex;
27
28use crate::{
29    cli, event, parser,
30    runner::{self, basic::RetryOptions},
31    step,
32    tag::Ext as _,
33    writer, Event, Parser, Runner, ScenarioType, Step, World, Writer,
34    WriterExt as _,
35};
36
37/// Top-level [Cucumber] executor.
38///
39/// Most of the time you don't need to work with it directly, just use
40/// [`World::run()`] or [`World::cucumber()`] on your [`World`] deriver to get
41/// [Cucumber] up and running.
42///
43/// Otherwise use [`Cucumber::new()`] to get the default [Cucumber] executor,
44/// provide [`Step`]s with [`World::collection()`] or by hand with
45/// [`Cucumber::given()`], [`Cucumber::when()`] and [`Cucumber::then()`].
46///
47/// In case you want a custom [`Parser`], [`Runner`] or [`Writer`], or some
48/// other finer control, use [`Cucumber::custom()`] or
49/// [`Cucumber::with_parser()`], [`Cucumber::with_runner()`] and
50/// [`Cucumber::with_writer()`] to construct your dream [Cucumber] executor!
51///
52/// [Cucumber]: https://cucumber.io
53pub struct Cucumber<W, P, I, R, Wr, Cli = cli::Empty>
54where
55    W: World,
56    P: Parser<I>,
57    R: Runner<W>,
58    Wr: Writer<W>,
59    Cli: clap::Args,
60{
61    /// [`Parser`] sourcing [`Feature`]s for execution.
62    ///
63    /// [`Feature`]: gherkin::Feature
64    parser: P,
65
66    /// [`Runner`] executing [`Scenario`]s and producing [`event`]s.
67    ///
68    /// [`Scenario`]: gherkin::Scenario
69    pub(crate) runner: R,
70
71    /// [`Writer`] outputting [`event`]s to some output.
72    writer: Wr,
73
74    /// CLI options this [`Cucumber`] has been run with.
75    ///
76    /// If empty, then will be parsed from a command line.
77    cli: Option<cli::Opts<P::Cli, R::Cli, Wr::Cli, Cli>>,
78
79    /// Type of the [`World`] this [`Cucumber`] run on.
80    _world: PhantomData<W>,
81
82    /// Type of the input consumed by [`Cucumber::parser`].
83    _parser_input: PhantomData<I>,
84}
85
86impl<W, P, I, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
87where
88    W: World,
89    P: Parser<I>,
90    R: Runner<W>,
91    Wr: Writer<W>,
92    Cli: clap::Args,
93{
94    /// Creates a custom [`Cucumber`] executor with the provided [`Parser`],
95    /// [`Runner`] and [`Writer`].
96    #[must_use]
97    pub const fn custom(parser: P, runner: R, writer: Wr) -> Self {
98        Self {
99            parser,
100            runner,
101            writer,
102            cli: None,
103            _world: PhantomData,
104            _parser_input: PhantomData,
105        }
106    }
107
108    /// Replaces [`Parser`].
109    #[must_use]
110    pub fn with_parser<NewP, NewI>(
111        self,
112        parser: NewP,
113    ) -> Cucumber<W, NewP, NewI, R, Wr, Cli>
114    where
115        NewP: Parser<NewI>,
116    {
117        let Self { runner, writer, .. } = self;
118        Cucumber {
119            parser,
120            runner,
121            writer,
122            cli: None,
123            _world: PhantomData,
124            _parser_input: PhantomData,
125        }
126    }
127
128    /// Replaces [`Runner`].
129    #[must_use]
130    pub fn with_runner<NewR>(
131        self,
132        runner: NewR,
133    ) -> Cucumber<W, P, I, NewR, Wr, Cli>
134    where
135        NewR: Runner<W>,
136    {
137        let Self { parser, writer, .. } = self;
138        Cucumber {
139            parser,
140            runner,
141            writer,
142            cli: None,
143            _world: PhantomData,
144            _parser_input: PhantomData,
145        }
146    }
147
148    /// Replaces [`Writer`].
149    #[must_use]
150    pub fn with_writer<NewWr>(
151        self,
152        writer: NewWr,
153    ) -> Cucumber<W, P, I, R, NewWr, Cli>
154    where
155        NewWr: Writer<W>,
156    {
157        let Self { parser, runner, .. } = self;
158        Cucumber {
159            parser,
160            runner,
161            writer,
162            cli: None,
163            _world: PhantomData,
164            _parser_input: PhantomData,
165        }
166    }
167
168    /// Re-outputs [`Skipped`] steps for easier navigation.
169    ///
170    /// # Example
171    ///
172    /// Output with a regular [`Cucumber::run()`]:
173    /// <script
174    ///     id="asciicast-0d92qlT8Mbc4WXyvRbHJmjsqN"
175    ///     src="https://asciinema.org/a/0d92qlT8Mbc4WXyvRbHJmjsqN.js"
176    ///     async data-autoplay="true" data-rows="17">
177    /// </script>
178    ///
179    /// Adjust [`Cucumber`] to re-output all the [`Skipped`] steps at the end:
180    /// ```rust
181    /// # use cucumber::World;
182    /// #
183    /// # #[derive(Debug, Default, World)]
184    /// # struct MyWorld;
185    /// #
186    /// # #[tokio::main(flavor = "current_thread")]
187    /// # async fn main() {
188    /// MyWorld::cucumber()
189    ///     .repeat_skipped()
190    ///     .run_and_exit("tests/features/readme")
191    ///     .await;
192    /// # }
193    /// ```
194    /// <script
195    ///     id="asciicast-ox14HynkBIw8atpfhyfvKrsO3"
196    ///     src="https://asciinema.org/a/ox14HynkBIw8atpfhyfvKrsO3.js"
197    ///     async data-autoplay="true" data-rows="19">
198    /// </script>
199    ///
200    /// [`Scenario`]: gherkin::Scenario
201    /// [`Skipped`]: event::Step::Skipped
202    #[must_use]
203    pub fn repeat_skipped(
204        self,
205    ) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr>, Cli>
206    where
207        Wr: writer::NonTransforming,
208    {
209        Cucumber {
210            parser: self.parser,
211            runner: self.runner,
212            writer: self.writer.repeat_skipped(),
213            cli: self.cli,
214            _world: PhantomData,
215            _parser_input: PhantomData,
216        }
217    }
218
219    /// Re-outputs [`Failed`] steps for easier navigation.
220    ///
221    /// # Example
222    ///
223    /// Output with a regular [`Cucumber::fail_on_skipped()`]:
224    /// ```rust,should_panic
225    /// # use cucumber::World;
226    /// #
227    /// # #[derive(Debug, Default, World)]
228    /// # struct MyWorld;
229    /// #
230    /// # #[tokio::main(flavor = "current_thread")]
231    /// # async fn main() {
232    /// MyWorld::cucumber()
233    ///     .fail_on_skipped()
234    ///     .run_and_exit("tests/features/readme")
235    ///     .await;
236    /// # }
237    /// ```
238    /// <script
239    ///     id="asciicast-UcipuopO6IFEsIDty6vaJlCH9"
240    ///     src="https://asciinema.org/a/UcipuopO6IFEsIDty6vaJlCH9.js"
241    ///     async data-autoplay="true" data-rows="21">
242    /// </script>
243    ///
244    /// Adjust [`Cucumber`] to re-output all the [`Failed`] steps at the end:
245    /// ```rust,should_panic
246    /// # use cucumber::World;
247    /// #
248    /// # #[derive(Debug, Default, World)]
249    /// # struct MyWorld;
250    /// #
251    /// # #[tokio::main(flavor = "current_thread")]
252    /// # async fn main() {
253    /// MyWorld::cucumber()
254    ///     .repeat_failed()
255    ///     .fail_on_skipped()
256    ///     .run_and_exit("tests/features/readme")
257    ///     .await;
258    /// # }
259    /// ```
260    /// <script
261    ///     id="asciicast-ofOljvyEMb41OTLhE081QKv68"
262    ///     src="https://asciinema.org/a/ofOljvyEMb41OTLhE081QKv68.js"
263    ///     async data-autoplay="true" data-rows="24">
264    /// </script>
265    ///
266    /// [`Failed`]: event::Step::Failed
267    #[must_use]
268    pub fn repeat_failed(
269        self,
270    ) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr>, Cli>
271    where
272        Wr: writer::NonTransforming,
273    {
274        Cucumber {
275            parser: self.parser,
276            runner: self.runner,
277            writer: self.writer.repeat_failed(),
278            cli: self.cli,
279            _world: PhantomData,
280            _parser_input: PhantomData,
281        }
282    }
283
284    /// Re-outputs steps by the given `filter` predicate.
285    ///
286    /// # Example
287    ///
288    /// Output with a regular [`Cucumber::fail_on_skipped()`]:
289    /// ```rust,should_panic
290    /// # use cucumber::World;
291    /// # use futures::FutureExt as _;
292    /// #
293    /// # #[derive(Debug, Default, World)]
294    /// # struct MyWorld;
295    /// #
296    /// # #[tokio::main(flavor = "current_thread")]
297    /// # async fn main() {
298    /// MyWorld::cucumber()
299    ///     .fail_on_skipped()
300    ///     .run_and_exit("tests/features/readme")
301    ///     .await;
302    /// # }
303    /// ```
304    /// <script
305    ///     id="asciicast-UcipuopO6IFEsIDty6vaJlCH9"
306    ///     src="https://asciinema.org/a/UcipuopO6IFEsIDty6vaJlCH9.js"
307    ///     async data-autoplay="true" data-rows="21">
308    /// </script>
309    ///
310    /// Adjust [`Cucumber`] to re-output all the [`Failed`] steps ta the end by
311    /// providing a custom `filter` predicate:
312    /// ```rust,should_panic
313    /// # use cucumber::World;
314    /// #
315    /// # #[derive(Debug, Default, World)]
316    /// # struct MyWorld;
317    /// #
318    /// # #[tokio::main(flavor = "current_thread")]
319    /// # async fn main() {
320    /// MyWorld::cucumber()
321    ///     .repeat_if(|ev| {
322    ///         use cucumber::event::{
323    ///             Cucumber, Feature, RetryableScenario, Rule, Scenario, Step,
324    ///         };
325    ///
326    ///         matches!(
327    ///             ev.as_deref(),
328    ///             Ok(Cucumber::Feature(
329    ///                 _,
330    ///                 Feature::Rule(
331    ///                     _,
332    ///                     Rule::Scenario(
333    ///                         _,
334    ///                         RetryableScenario {
335    ///                             event: Scenario::Step(_, Step::Failed(..))
336    ///                                 | Scenario::Background(
337    ///                                     _,
338    ///                                     Step::Failed(_, _, _, _),
339    ///                                 ),
340    ///                             retries: _
341    ///                         }
342    ///                     )
343    ///                 ) | Feature::Scenario(
344    ///                     _,
345    ///                     RetryableScenario {
346    ///                         event: Scenario::Step(_, Step::Failed(..))
347    ///                             | Scenario::Background(_, Step::Failed(..)),
348    ///                         retries: _
349    ///                     }
350    ///                 )
351    ///             )) | Err(_)
352    ///         )
353    ///     })
354    ///     .fail_on_skipped()
355    ///     .run_and_exit("tests/features/readme")
356    ///     .await;
357    /// # }
358    /// ```
359    /// <script
360    ///     id="asciicast-ofOljvyEMb41OTLhE081QKv68"
361    ///     src="https://asciinema.org/a/ofOljvyEMb41OTLhE081QKv68.js"
362    ///     async data-autoplay="true" data-rows="24">
363    /// </script>
364    ///
365    /// [`Failed`]: event::Step::Failed
366    #[must_use]
367    pub fn repeat_if<F>(
368        self,
369        filter: F,
370    ) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr, F>, Cli>
371    where
372        F: Fn(&parser::Result<Event<event::Cucumber<W>>>) -> bool,
373        Wr: writer::NonTransforming,
374    {
375        Cucumber {
376            parser: self.parser,
377            runner: self.runner,
378            writer: self.writer.repeat_if(filter),
379            cli: self.cli,
380            _world: PhantomData,
381            _parser_input: PhantomData,
382        }
383    }
384
385    /// Consider [`Skipped`] [`Background`] or regular [`Step`]s as [`Failed`]
386    /// if their [`Scenario`] isn't marked with `@allow.skipped` tag.
387    ///
388    /// It's useful option for ensuring that all the steps were covered.
389    ///
390    /// # Example
391    ///
392    /// Output with a regular [`Cucumber::run()`]:
393    /// <script
394    ///     id="asciicast-0d92qlT8Mbc4WXyvRbHJmjsqN"
395    ///     src="https://asciinema.org/a/0d92qlT8Mbc4WXyvRbHJmjsqN.js"
396    ///     async data-autoplay="true" data-rows="17">
397    /// </script>
398    ///
399    /// To fail all the [`Skipped`] steps setup [`Cucumber`] like this:
400    /// ```rust,should_panic
401    /// # use cucumber::World;
402    /// #
403    /// # #[derive(Debug, Default, World)]
404    /// # struct MyWorld;
405    /// #
406    /// # #[tokio::main(flavor = "current_thread")]
407    /// # async fn main() {
408    /// MyWorld::cucumber()
409    ///     .fail_on_skipped()
410    ///     .run_and_exit("tests/features/readme")
411    ///     .await;
412    /// # }
413    /// ```
414    /// <script
415    ///     id="asciicast-IHLxMEgku9BtBVkR4k2DtOjMd"
416    ///     src="https://asciinema.org/a/IHLxMEgku9BtBVkR4k2DtOjMd.js"
417    ///     async data-autoplay="true" data-rows="21">
418    /// </script>
419    ///
420    /// To intentionally suppress some [`Skipped`] steps failing, use the
421    /// `@allow.skipped` tag:
422    /// ```gherkin
423    /// Feature: Animal feature
424    ///
425    ///   Scenario: If we feed a hungry cat it will no longer be hungry
426    ///     Given a hungry cat
427    ///     When I feed the cat
428    ///     Then the cat is not hungry
429    ///
430    ///   @allow.skipped
431    ///   Scenario: If we feed a satiated dog it will not become hungry
432    ///     Given a satiated dog
433    ///     When I feed the dog
434    ///     Then the dog is not hungry
435    /// ```
436    ///
437    /// [`Background`]: gherkin::Background
438    /// [`Failed`]: event::Step::Failed
439    /// [`Scenario`]: gherkin::Scenario
440    /// [`Skipped`]: event::Step::Skipped
441    /// [`Step`]: gherkin::Step
442    #[must_use]
443    pub fn fail_on_skipped(
444        self,
445    ) -> Cucumber<W, P, I, R, writer::FailOnSkipped<Wr>, Cli> {
446        Cucumber {
447            parser: self.parser,
448            runner: self.runner,
449            writer: self.writer.fail_on_skipped(),
450            cli: self.cli,
451            _world: PhantomData,
452            _parser_input: PhantomData,
453        }
454    }
455
456    /// Consider [`Skipped`] [`Background`] or regular [`Step`]s as [`Failed`]
457    /// if the given `filter` predicate returns `true`.
458    ///
459    /// # Example
460    ///
461    /// Output with a regular [`Cucumber::run()`]:
462    /// <script
463    ///     id="asciicast-0d92qlT8Mbc4WXyvRbHJmjsqN"
464    ///     src="https://asciinema.org/a/0d92qlT8Mbc4WXyvRbHJmjsqN.js"
465    ///     async data-autoplay="true" data-rows="17">
466    /// </script>
467    ///
468    /// Adjust [`Cucumber`] to fail on all [`Skipped`] steps, but the ones
469    /// marked with a `@dog` tag:
470    /// ```rust,should_panic
471    /// # use cucumber::World;
472    /// #
473    /// # #[derive(Debug, Default, World)]
474    /// # struct MyWorld;
475    /// #
476    /// # #[tokio::main(flavor = "current_thread")]
477    /// # async fn main() {
478    /// MyWorld::cucumber()
479    ///     .fail_on_skipped_with(|_, _, s| !s.tags.iter().any(|t| t == "dog"))
480    ///     .run_and_exit("tests/features/readme")
481    ///     .await;
482    /// # }
483    /// ```
484    /// ```gherkin
485    /// Feature: Animal feature
486    ///
487    ///   Scenario: If we feed a hungry cat it will no longer be hungry
488    ///     Given a hungry cat
489    ///     When I feed the cat
490    ///     Then the cat is not hungry
491    ///
492    ///   Scenario: If we feed a satiated dog it will not become hungry
493    ///     Given a satiated dog
494    ///     When I feed the dog
495    ///     Then the dog is not hungry
496    /// ```
497    /// <script
498    ///     id="asciicast-IHLxMEgku9BtBVkR4k2DtOjMd"
499    ///     src="https://asciinema.org/a/IHLxMEgku9BtBVkR4k2DtOjMd.js"
500    ///     async data-autoplay="true" data-rows="21">
501    /// </script>
502    ///
503    /// And to avoid failing, use the `@dog` tag:
504    /// ```gherkin
505    /// Feature: Animal feature
506    ///
507    ///   Scenario: If we feed a hungry cat it will no longer be hungry
508    ///     Given a hungry cat
509    ///     When I feed the cat
510    ///     Then the cat is not hungry
511    ///
512    ///   @dog
513    ///   Scenario: If we feed a satiated dog it will not become hungry
514    ///     Given a satiated dog
515    ///     When I feed the dog
516    ///     Then the dog is not hungry
517    /// ```
518    ///
519    /// [`Background`]: gherkin::Background
520    /// [`Failed`]: event::Step::Failed
521    /// [`Scenario`]: gherkin::Scenario
522    /// [`Skipped`]: event::Step::Skipped
523    /// [`Step`]: gherkin::Step
524    #[must_use]
525    pub fn fail_on_skipped_with<Filter>(
526        self,
527        filter: Filter,
528    ) -> Cucumber<W, P, I, R, writer::FailOnSkipped<Wr, Filter>, Cli>
529    where
530        Filter: Fn(
531            &gherkin::Feature,
532            Option<&gherkin::Rule>,
533            &gherkin::Scenario,
534        ) -> bool,
535    {
536        Cucumber {
537            parser: self.parser,
538            runner: self.runner,
539            writer: self.writer.fail_on_skipped_with(filter),
540            cli: self.cli,
541            _world: PhantomData,
542            _parser_input: PhantomData,
543        }
544    }
545}
546
547impl<W, P, I, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
548where
549    W: World,
550    P: Parser<I>,
551    R: Runner<W>,
552    Wr: Writer<W> + writer::Normalized,
553    Cli: clap::Args,
554{
555    /// Runs [`Cucumber`].
556    ///
557    /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which
558    /// produces events handled by a [`Writer`].
559    ///
560    /// [`Feature`]: gherkin::Feature
561    pub async fn run(self, input: I) -> Wr {
562        self.filter_run(input, |_, _, _| true).await
563    }
564
565    /// Consumes already parsed [`cli::Opts`].
566    ///
567    /// This method allows to pre-parse [`cli::Opts`] for custom needs before
568    /// using them inside [`Cucumber`].
569    ///
570    /// Also, any additional custom CLI options may be specified as a
571    /// [`clap::Args`] deriving type, used as the last type parameter of
572    /// [`cli::Opts`].
573    ///
574    /// > ⚠️ __WARNING__: Any CLI options of [`Parser`], [`Runner`], [`Writer`]
575    ///                   or custom ones should not overlap, otherwise
576    ///                   [`cli::Opts`] will fail to parse on startup.
577    ///
578    /// # Example
579    ///
580    /// ```rust
581    /// # use std::time::Duration;
582    /// #
583    /// # use cucumber::{cli, World};
584    /// # use futures::FutureExt as _;
585    /// # use tokio::time;
586    /// #
587    /// # #[derive(Debug, Default, World)]
588    /// # struct MyWorld;
589    /// #
590    /// # #[tokio::main(flavor = "current_thread")]
591    /// # async fn main() {
592    /// #[derive(clap::Args)]
593    /// struct CustomCli {
594    ///     /// Additional time to wait in a before hook.
595    ///     #[arg(
596    ///         long,
597    ///         value_parser = humantime::parse_duration,
598    ///     )]
599    ///     before_time: Option<Duration>,
600    /// }
601    ///
602    /// let cli = cli::Opts::<_, _, _, CustomCli>::parsed();
603    /// let time = cli.custom.before_time.unwrap_or_default();
604    ///
605    /// MyWorld::cucumber()
606    ///     .before(move |_, _, _, _| time::sleep(time).boxed_local())
607    ///     .with_cli(cli)
608    ///     .run_and_exit("tests/features/readme")
609    ///     .await;
610    /// # }
611    /// ```
612    /// ```gherkin
613    /// Feature: Animal feature
614    ///
615    ///   Scenario: If we feed a hungry cat it will no longer be hungry
616    ///     Given a hungry cat
617    ///     When I feed the cat
618    ///     Then the cat is not hungry
619    /// ```
620    /// <script
621    ///     id="asciicast-0KvTxnfaMRjsvsIKsalS611Ta"
622    ///     src="https://asciinema.org/a/0KvTxnfaMRjsvsIKsalS611Ta.js"
623    ///     async data-autoplay="true" data-rows="14">
624    /// </script>
625    ///
626    /// Also, specifying `--help` flag will describe `--before-time` now.
627    ///
628    /// [`Feature`]: gherkin::Feature
629    #[must_use]
630    pub fn with_cli<CustomCli>(
631        self,
632        cli: cli::Opts<P::Cli, R::Cli, Wr::Cli, CustomCli>,
633    ) -> Cucumber<W, P, I, R, Wr, CustomCli>
634    where
635        CustomCli: clap::Args,
636    {
637        let Self {
638            parser,
639            runner,
640            writer,
641            ..
642        } = self;
643        Cucumber {
644            parser,
645            runner,
646            writer,
647            cli: Some(cli),
648            _world: PhantomData,
649            _parser_input: PhantomData,
650        }
651    }
652
653    /// Initializes [`Default`] [`cli::Opts`].
654    ///
655    /// This method allows to omit parsing real [`cli::Opts`], as eagerly
656    /// initializes [`Default`] ones instead.
657    #[must_use]
658    pub fn with_default_cli(mut self) -> Self
659    where
660        cli::Opts<P::Cli, R::Cli, Wr::Cli, Cli>: Default,
661    {
662        self.cli = Some(cli::Opts::default());
663        self
664    }
665
666    /// Runs [`Cucumber`] with [`Scenario`]s filter.
667    ///
668    /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which
669    /// produces events handled by a [`Writer`].
670    ///
671    /// # Example
672    ///
673    /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag:
674    /// ```rust
675    /// # use cucumber::World;
676    /// #
677    /// # #[derive(Debug, Default, World)]
678    /// # struct MyWorld;
679    /// #
680    /// # #[tokio::main(flavor = "current_thread")]
681    /// # async fn main() {
682    /// MyWorld::cucumber()
683    ///     .filter_run("tests/features/readme", |_, _, sc| {
684    ///         sc.tags.iter().any(|t| t == "cat")
685    ///     })
686    ///     .await;
687    /// # }
688    /// ```
689    /// ```gherkin
690    /// Feature: Animal feature
691    ///
692    ///   @cat
693    ///   Scenario: If we feed a hungry cat it will no longer be hungry
694    ///     Given a hungry cat
695    ///     When I feed the cat
696    ///     Then the cat is not hungry
697    ///
698    ///   @dog
699    ///   Scenario: If we feed a satiated dog it will not become hungry
700    ///     Given a satiated dog
701    ///     When I feed the dog
702    ///     Then the dog is not hungry
703    /// ```
704    /// <script
705    ///     id="asciicast-0KvTxnfaMRjsvsIKsalS611Ta"
706    ///     src="https://asciinema.org/a/0KvTxnfaMRjsvsIKsalS611Ta.js"
707    ///     async data-autoplay="true" data-rows="14">
708    /// </script>
709    ///
710    /// [`Feature`]: gherkin::Feature
711    /// [`Scenario`]: gherkin::Scenario
712    pub async fn filter_run<F>(self, input: I, filter: F) -> Wr
713    where
714        F: Fn(
715                &gherkin::Feature,
716                Option<&gherkin::Rule>,
717                &gherkin::Scenario,
718            ) -> bool
719            + 'static,
720    {
721        let cli::Opts {
722            re_filter,
723            tags_filter,
724            parser: parser_cli,
725            runner: runner_cli,
726            writer: writer_cli,
727            ..
728        } = self.cli.unwrap_or_else(cli::Opts::<_, _, _, _>::parsed);
729
730        let filter = move |feat: &gherkin::Feature,
731                           rule: Option<&gherkin::Rule>,
732                           scenario: &gherkin::Scenario| {
733            re_filter.as_ref().map_or_else(
734                || {
735                    tags_filter.as_ref().map_or_else(
736                        || filter(feat, rule, scenario),
737                        |tags| {
738                            // The order `Feature` -> `Rule` -> `Scenario`
739                            // matters here.
740                            tags.eval(
741                                feat.tags
742                                    .iter()
743                                    .chain(rule.iter().flat_map(|r| &r.tags))
744                                    .chain(scenario.tags.iter()),
745                            )
746                        },
747                    )
748                },
749                |re| re.is_match(&scenario.name),
750            )
751        };
752
753        let Self {
754            parser,
755            runner,
756            mut writer,
757            ..
758        } = self;
759
760        let features = parser.parse(input, parser_cli);
761
762        let filtered = features.map(move |feature| {
763            let mut feature = feature?;
764            let feat_scenarios = mem::take(&mut feature.scenarios);
765            feature.scenarios = feat_scenarios
766                .into_iter()
767                .filter(|s| filter(&feature, None, s))
768                .collect();
769
770            let mut rules = mem::take(&mut feature.rules);
771            for r in &mut rules {
772                let rule_scenarios = mem::take(&mut r.scenarios);
773                r.scenarios = rule_scenarios
774                    .into_iter()
775                    .filter(|s| filter(&feature, Some(r), s))
776                    .collect();
777            }
778            feature.rules = rules;
779
780            Ok(feature)
781        });
782
783        let events_stream = runner.run(filtered, runner_cli);
784        futures::pin_mut!(events_stream);
785        while let Some(ev) = events_stream.next().await {
786            writer.handle_event(ev, &writer_cli).await;
787        }
788        writer
789    }
790}
791
792// Implemented manually to omit redundant `W: Clone` and `I: Clone` trait
793// bounds, imposed by `#[derive(Clone)]`.
794impl<W, P, I, R, Wr, Cli> Clone for Cucumber<W, P, I, R, Wr, Cli>
795where
796    W: World,
797    P: Clone + Parser<I>,
798    R: Clone + Runner<W>,
799    Wr: Clone + Writer<W>,
800    Cli: Clone + clap::Args,
801    P::Cli: Clone,
802    R::Cli: Clone,
803    Wr::Cli: Clone,
804{
805    fn clone(&self) -> Self {
806        Self {
807            parser: self.parser.clone(),
808            runner: self.runner.clone(),
809            writer: self.writer.clone(),
810            cli: self.cli.clone(),
811            _world: PhantomData,
812            _parser_input: PhantomData,
813        }
814    }
815}
816
817impl<W, P, I, R, Wr, Cli> Debug for Cucumber<W, P, I, R, Wr, Cli>
818where
819    W: World,
820    P: Debug + Parser<I>,
821    <P as Parser<I>>::Cli: Debug,
822    R: Debug + Runner<W>,
823    <R as Runner<W>>::Cli: Debug,
824    Wr: Debug + Writer<W>,
825    <Wr as Writer<W>>::Cli: Debug,
826    Cli: clap::Args + Debug,
827{
828    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
829        f.debug_struct("Cucumber")
830            .field("parser", &self.parser)
831            .field("runner", &self.runner)
832            .field("writer", &self.writer)
833            .field("cli", &self.cli)
834            .finish()
835    }
836}
837
838/// Shortcut for the [`Cucumber`] type returned by its [`Default`] impl.
839pub(crate) type DefaultCucumber<W, I> = Cucumber<
840    W,
841    parser::Basic,
842    I,
843    runner::Basic<W>,
844    writer::Summarize<writer::Normalize<W, writer::Basic>>,
845>;
846
847impl<W, I> Default for DefaultCucumber<W, I>
848where
849    W: World + Debug,
850    I: AsRef<Path>,
851{
852    fn default() -> Self {
853        Self::custom(
854            parser::Basic::new(),
855            runner::Basic::default(),
856            writer::Basic::stdout().summarized(),
857        )
858    }
859}
860
861impl<W, I> DefaultCucumber<W, I>
862where
863    W: World + Debug,
864    I: AsRef<Path>,
865{
866    /// Creates a default [`Cucumber`] executor.
867    ///
868    /// * [`Parser`] — [`parser::Basic`]
869    ///
870    /// * [`Runner`] — [`runner::Basic`]
871    ///   * [`ScenarioType`] — [`Concurrent`] by default, [`Serial`] if
872    ///     `@serial` [tag] is present on a [`Scenario`];
873    ///   * Allowed to run up to 64 [`Concurrent`] [`Scenario`]s.
874    ///
875    /// * [`Writer`] — [`Normalize`] and [`Summarize`] [`writer::Basic`].
876    ///
877    /// [`Concurrent`]: ScenarioType::Concurrent
878    /// [`Normalize`]: writer::Normalize
879    /// [`Scenario`]: gherkin::Scenario
880    /// [`Serial`]: ScenarioType::Serial
881    /// [`Summarize`]: writer::Summarize
882    ///
883    /// [tag]: https://cucumber.io/docs/cucumber/api#tags
884    #[must_use]
885    pub fn new() -> Self {
886        Self::default()
887    }
888}
889
890impl<W, I, R, Wr, Cli> Cucumber<W, parser::Basic, I, R, Wr, Cli>
891where
892    W: World,
893    R: Runner<W>,
894    Wr: Writer<W>,
895    Cli: clap::Args,
896    I: AsRef<Path>,
897{
898    /// Sets the provided language of [`gherkin`] files.
899    ///
900    /// # Errors
901    ///
902    /// If the provided language isn't supported.
903    pub fn language(
904        mut self,
905        name: impl Into<Cow<'static, str>>,
906    ) -> Result<Self, parser::basic::UnsupportedLanguageError> {
907        self.parser = self.parser.language(name)?;
908        Ok(self)
909    }
910}
911
912impl<W, I, P, Wr, F, B, A, Cli>
913    Cucumber<W, P, I, runner::Basic<W, F, B, A>, Wr, Cli>
914where
915    W: World,
916    P: Parser<I>,
917    Wr: Writer<W>,
918    Cli: clap::Args,
919    F: Fn(
920            &gherkin::Feature,
921            Option<&gherkin::Rule>,
922            &gherkin::Scenario,
923        ) -> ScenarioType
924        + 'static,
925    B: for<'a> Fn(
926            &'a gherkin::Feature,
927            Option<&'a gherkin::Rule>,
928            &'a gherkin::Scenario,
929            &'a mut W,
930        ) -> LocalBoxFuture<'a, ()>
931        + 'static,
932    A: for<'a> Fn(
933            &'a gherkin::Feature,
934            Option<&'a gherkin::Rule>,
935            &'a gherkin::Scenario,
936            &'a event::ScenarioFinished,
937            Option<&'a mut W>,
938        ) -> LocalBoxFuture<'a, ()>
939        + 'static,
940{
941    /// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will
942    /// be limited.
943    ///
944    /// [`Scenario`]: gherkin::Scenario
945    #[must_use]
946    pub fn max_concurrent_scenarios(
947        mut self,
948        max: impl Into<Option<usize>>,
949    ) -> Self {
950        self.runner = self.runner.max_concurrent_scenarios(max);
951        self
952    }
953
954    /// Makes failed [`Scenario`]s being retried the specified number of times.
955    ///
956    /// [`Scenario`]: gherkin::Scenario
957    #[must_use]
958    pub fn retries(mut self, retries: impl Into<Option<usize>>) -> Self {
959        self.runner = self.runner.retries(retries);
960        self
961    }
962
963    /// Makes stop running tests on the first failure.
964    ///
965    /// __NOTE__: All the already started [`Scenario`]s at the moment of failure
966    ///           will be finished.
967    ///
968    /// __NOTE__: Retried [`Scenario`]s are considered as failed, only in case
969    ///           they exhaust all retry attempts and still do fail.
970    ///
971    /// [`Scenario`]: gherkin::Scenario
972    #[must_use]
973    pub fn fail_fast(mut self) -> Self {
974        self.runner = self.runner.fail_fast();
975        self
976    }
977
978    /// Makes failed [`Scenario`]s being retried after the specified
979    /// [`Duration`] passes.
980    ///
981    /// [`Scenario`]: gherkin::Scenario
982    #[must_use]
983    pub fn retry_after(mut self, after: impl Into<Option<Duration>>) -> Self {
984        self.runner = self.runner.retry_after(after);
985        self
986    }
987
988    /// Makes failed [`Scenario`]s being retried only if they're matching the
989    /// specified `tag_expression`.
990    ///
991    /// [`Scenario`]: gherkin::Scenario
992    #[must_use]
993    pub fn retry_filter(
994        mut self,
995        tag_expression: impl Into<Option<TagOperation>>,
996    ) -> Self {
997        self.runner = self.runner.retry_filter(tag_expression);
998        self
999    }
1000
1001    /// Function determining whether a [`Scenario`] is [`Concurrent`] or
1002    /// a [`Serial`] one.
1003    ///
1004    /// [`Concurrent`]: ScenarioType::Concurrent
1005    /// [`Serial`]: ScenarioType::Serial
1006    /// [`Scenario`]: gherkin::Scenario
1007    #[must_use]
1008    pub fn which_scenario<Which>(
1009        self,
1010        func: Which,
1011    ) -> Cucumber<W, P, I, runner::Basic<W, Which, B, A>, Wr, Cli>
1012    where
1013        Which: Fn(
1014                &gherkin::Feature,
1015                Option<&gherkin::Rule>,
1016                &gherkin::Scenario,
1017            ) -> ScenarioType
1018            + 'static,
1019    {
1020        let Self {
1021            parser,
1022            runner,
1023            writer,
1024            cli,
1025            ..
1026        } = self;
1027        Cucumber {
1028            parser,
1029            runner: runner.which_scenario(func),
1030            writer,
1031            cli,
1032            _world: PhantomData,
1033            _parser_input: PhantomData,
1034        }
1035    }
1036
1037    /// Function determining [`Scenario`]'s [`RetryOptions`].
1038    ///
1039    /// [`Scenario`]: gherkin::Scenario
1040    #[must_use]
1041    pub fn retry_options<Retry>(mut self, func: Retry) -> Self
1042    where
1043        Retry: Fn(
1044                &gherkin::Feature,
1045                Option<&gherkin::Rule>,
1046                &gherkin::Scenario,
1047                &runner::basic::Cli,
1048            ) -> Option<RetryOptions>
1049            + 'static,
1050    {
1051        self.runner = self.runner.retry_options(func);
1052        self
1053    }
1054
1055    /// Sets a hook, executed on each [`Scenario`] before running all its
1056    /// [`Step`]s, including [`Background`] ones.
1057    ///
1058    /// [`Background`]: gherkin::Background
1059    /// [`Scenario`]: gherkin::Scenario
1060    /// [`Step`]: gherkin::Step
1061    #[must_use]
1062    pub fn before<Before>(
1063        self,
1064        func: Before,
1065    ) -> Cucumber<W, P, I, runner::Basic<W, F, Before, A>, Wr, Cli>
1066    where
1067        Before: for<'a> Fn(
1068                &'a gherkin::Feature,
1069                Option<&'a gherkin::Rule>,
1070                &'a gherkin::Scenario,
1071                &'a mut W,
1072            ) -> LocalBoxFuture<'a, ()>
1073            + 'static,
1074    {
1075        let Self {
1076            parser,
1077            runner,
1078            writer,
1079            cli,
1080            ..
1081        } = self;
1082        Cucumber {
1083            parser,
1084            runner: runner.before(func),
1085            writer,
1086            cli,
1087            _world: PhantomData,
1088            _parser_input: PhantomData,
1089        }
1090    }
1091
1092    /// Sets a hook, executed on each [`Scenario`] after running all its
1093    /// [`Step`]s, even after [`Skipped`] of [`Failed`] [`Step`]s.
1094    ///
1095    /// Last `World` argument is supplied to the function, in case it was
1096    /// initialized before by running [`before`] hook or any [`Step`].
1097    ///
1098    /// [`before`]: Self::before()
1099    /// [`Failed`]: event::Step::Failed
1100    /// [`Scenario`]: gherkin::Scenario
1101    /// [`Skipped`]: event::Step::Skipped
1102    /// [`Step`]: gherkin::Step
1103    #[must_use]
1104    pub fn after<After>(
1105        self,
1106        func: After,
1107    ) -> Cucumber<W, P, I, runner::Basic<W, F, B, After>, Wr, Cli>
1108    where
1109        After: for<'a> Fn(
1110                &'a gherkin::Feature,
1111                Option<&'a gherkin::Rule>,
1112                &'a gherkin::Scenario,
1113                &'a event::ScenarioFinished,
1114                Option<&'a mut W>,
1115            ) -> LocalBoxFuture<'a, ()>
1116            + 'static,
1117    {
1118        let Self {
1119            parser,
1120            runner,
1121            writer,
1122            cli,
1123            ..
1124        } = self;
1125        Cucumber {
1126            parser,
1127            runner: runner.after(func),
1128            writer,
1129            cli,
1130            _world: PhantomData,
1131            _parser_input: PhantomData,
1132        }
1133    }
1134
1135    /// Replaces [`Collection`] of [`Step`]s.
1136    ///
1137    /// [`Collection`]: step::Collection
1138    /// [`Step`]: step::Step
1139    #[must_use]
1140    pub fn steps(mut self, steps: step::Collection<W>) -> Self {
1141        self.runner = self.runner.steps(steps);
1142        self
1143    }
1144
1145    /// Inserts [Given] [`Step`].
1146    ///
1147    /// [Given]: https://cucumber.io/docs/gherkin/reference#given
1148    #[must_use]
1149    pub fn given(mut self, regex: Regex, step: Step<W>) -> Self {
1150        self.runner = self.runner.given(regex, step);
1151        self
1152    }
1153
1154    /// Inserts [When] [`Step`].
1155    ///
1156    /// [When]: https://cucumber.io/docs/gherkin/reference#when
1157    #[must_use]
1158    pub fn when(mut self, regex: Regex, step: Step<W>) -> Self {
1159        self.runner = self.runner.when(regex, step);
1160        self
1161    }
1162
1163    /// Inserts [Then] [`Step`].
1164    ///
1165    /// [Then]: https://cucumber.io/docs/gherkin/reference#then
1166    #[must_use]
1167    pub fn then(mut self, regex: Regex, step: Step<W>) -> Self {
1168        self.runner = self.runner.then(regex, step);
1169        self
1170    }
1171}
1172
1173impl<W, I, P, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
1174where
1175    W: World,
1176    P: Parser<I>,
1177    R: Runner<W>,
1178    Wr: writer::Stats<W> + writer::Normalized,
1179    Cli: clap::Args,
1180{
1181    /// Runs [`Cucumber`].
1182    ///
1183    /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which
1184    /// produces events handled by a [`Writer`].
1185    ///
1186    /// # Panics
1187    ///
1188    /// If encountered errors while parsing [`Feature`]s or at least one
1189    /// [`Step`] [`Failed`].
1190    ///
1191    /// [`Failed`]: event::Step::Failed
1192    /// [`Feature`]: gherkin::Feature
1193    /// [`Step`]: gherkin::Step
1194    pub async fn run_and_exit(self, input: I) {
1195        self.filter_run_and_exit(input, |_, _, _| true).await;
1196    }
1197
1198    /// Runs [`Cucumber`] with [`Scenario`]s filter.
1199    ///
1200    /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which
1201    /// produces events handled by a [`Writer`].
1202    ///
1203    /// # Panics
1204    ///
1205    /// If encountered errors while parsing [`Feature`]s or at least one
1206    /// [`Step`] [`Failed`].
1207    ///
1208    /// # Example
1209    ///
1210    /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag:
1211    /// ```rust
1212    /// # use cucumber::World;
1213    /// #
1214    /// # #[derive(Debug, Default, World)]
1215    /// # struct MyWorld;
1216    /// #
1217    /// # #[tokio::main(flavor = "current_thread")]
1218    /// # async fn main() {
1219    /// MyWorld::cucumber()
1220    ///     .filter_run_and_exit("tests/features/readme", |_, _, sc| {
1221    ///         sc.tags.iter().any(|t| t == "cat")
1222    ///     })
1223    ///     .await;
1224    /// # }
1225    /// ```
1226    /// ```gherkin
1227    /// Feature: Animal feature
1228    ///
1229    ///   @cat
1230    ///   Scenario: If we feed a hungry cat it will no longer be hungry
1231    ///     Given a hungry cat
1232    ///     When I feed the cat
1233    ///     Then the cat is not hungry
1234    ///
1235    ///   @dog
1236    ///   Scenario: If we feed a satiated dog it will not become hungry
1237    ///     Given a satiated dog
1238    ///     When I feed the dog
1239    ///     Then the dog is not hungry
1240    /// ```
1241    /// <script
1242    ///     id="asciicast-0KvTxnfaMRjsvsIKsalS611Ta"
1243    ///     src="https://asciinema.org/a/0KvTxnfaMRjsvsIKsalS611Ta.js"
1244    ///     async data-autoplay="true" data-rows="14">
1245    /// </script>
1246    ///
1247    /// [`Failed`]: event::Step::Failed
1248    /// [`Feature`]: gherkin::Feature
1249    /// [`Scenario`]: gherkin::Scenario
1250    pub async fn filter_run_and_exit<Filter>(self, input: I, filter: Filter)
1251    where
1252        Filter: Fn(
1253                &gherkin::Feature,
1254                Option<&gherkin::Rule>,
1255                &gherkin::Scenario,
1256            ) -> bool
1257            + 'static,
1258    {
1259        let writer = self.filter_run(input, filter).await;
1260        if writer.execution_has_failed() {
1261            let mut msg = Vec::with_capacity(3);
1262
1263            let failed_steps = writer.failed_steps();
1264            if failed_steps > 0 {
1265                msg.push(format!(
1266                    "{failed_steps} step{} failed",
1267                    (failed_steps > 1).then_some("s").unwrap_or_default(),
1268                ));
1269            }
1270
1271            let parsing_errors = writer.parsing_errors();
1272            if parsing_errors > 0 {
1273                msg.push(format!(
1274                    "{parsing_errors} parsing error{}",
1275                    (parsing_errors > 1).then_some("s").unwrap_or_default(),
1276                ));
1277            }
1278
1279            let hook_errors = writer.hook_errors();
1280            if hook_errors > 0 {
1281                msg.push(format!(
1282                    "{hook_errors} hook error{}",
1283                    (hook_errors > 1).then_some("s").unwrap_or_default(),
1284                ));
1285            }
1286
1287            panic!("{}", msg.join(", "));
1288        }
1289    }
1290}