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}