Skip to main content

cucumber/runner/
basic.rs

1// Copyright (c) 2018-2026  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//! Default [`Runner`] implementation.
12
13use std::{
14    any::Any,
15    cmp,
16    collections::HashMap,
17    iter, mem,
18    ops::ControlFlow,
19    panic::{self, AssertUnwindSafe},
20    sync::{
21        Arc,
22        atomic::{AtomicBool, AtomicU64, Ordering},
23    },
24    thread,
25    time::{Duration, Instant},
26};
27
28#[cfg(feature = "tracing")]
29use crossbeam_utils::atomic::AtomicCell;
30use derive_more::with_trait::{Debug, Display, FromStr};
31use futures::{
32    FutureExt as _, Stream, StreamExt as _, TryFutureExt as _,
33    TryStreamExt as _,
34    channel::{mpsc, oneshot},
35    future::{self, Either, LocalBoxFuture},
36    lock::Mutex,
37    pin_mut,
38    stream::{self, LocalBoxStream},
39};
40use gherkin::tagexpr::TagOperation;
41use itertools::Itertools as _;
42use regex::{CaptureLocations, Regex};
43
44#[cfg(feature = "tracing")]
45use crate::tracing::{Collector as TracingCollector, SpanCloseWaiter};
46use crate::{
47    Event, Runner, Step, World,
48    event::{self, HookType, Info, Retries, Source},
49    feature::Ext as _,
50    future::{FutureExt as _, select_with_biased_first},
51    parser, step,
52    tag::Ext as _,
53};
54
55/// CLI options of a [`Basic`] [`Runner`].
56#[derive(Clone, Debug, Default, clap::Args)]
57#[group(skip)]
58pub struct Cli {
59    /// Number of scenarios to run concurrently. If not specified, uses the
60    /// value configured in tests runner, or 64 by default.
61    #[arg(long, short, value_name = "int", global = true)]
62    pub concurrency: Option<usize>,
63
64    /// Run tests until the first failure.
65    #[arg(long, global = true, visible_alias = "ff")]
66    pub fail_fast: bool,
67
68    /// Number of times a scenario will be retried in case of a failure.
69    #[arg(long, value_name = "int", global = true)]
70    pub retry: Option<usize>,
71
72    /// Delay between each scenario retry attempt.
73    ///
74    /// Duration is represented in a human-readable format like `12min5s`.
75    /// Supported suffixes:
76    /// - `nsec`, `ns` — nanoseconds.
77    /// - `usec`, `us` — microseconds.
78    /// - `msec`, `ms` — milliseconds.
79    /// - `seconds`, `second`, `sec`, `s` - seconds.
80    /// - `minutes`, `minute`, `min`, `m` - minutes.
81    #[arg(
82        long,
83        value_name = "duration",
84        value_parser = humantime::parse_duration,
85        verbatim_doc_comment,
86        global = true,
87    )]
88    pub retry_after: Option<Duration>,
89
90    /// Tag expression to filter retried scenarios.
91    #[arg(long, value_name = "tagexpr", global = true)]
92    pub retry_tag_filter: Option<TagOperation>,
93}
94
95/// Type determining whether [`Scenario`]s should run concurrently or
96/// sequentially.
97///
98/// [`Scenario`]: gherkin::Scenario
99#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
100pub enum ScenarioType {
101    /// Run [`Scenario`]s sequentially (one-by-one).
102    ///
103    /// [`Scenario`]: gherkin::Scenario
104    Serial,
105
106    /// Run [`Scenario`]s concurrently.
107    ///
108    /// [`Scenario`]: gherkin::Scenario
109    Concurrent,
110}
111
112/// Options for retrying [`Scenario`]s.
113///
114/// [`Scenario`]: gherkin::Scenario
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub struct RetryOptions {
117    /// Number of [`Retries`].
118    pub retries: Retries,
119
120    /// Delay before next retry attempt will be executed.
121    pub after: Option<Duration>,
122}
123
124impl RetryOptions {
125    /// Returns [`Some`], in case next retry attempt is available, or [`None`]
126    /// otherwise.
127    #[must_use]
128    pub fn next_try(self) -> Option<Self> {
129        self.retries
130            .next_try()
131            .map(|num| Self { retries: num, after: self.after })
132    }
133
134    /// Parses [`RetryOptions`] from [`Feature`]'s, [`Rule`]'s, [`Scenario`]'s
135    /// tags and [`Cli`] options.
136    ///
137    /// [`Feature`]: gherkin::Feature
138    /// [`Rule`]: gherkin::Rule
139    /// [`Scenario`]: gherkin::Scenario
140    #[must_use]
141    pub fn parse_from_tags(
142        feature: &gherkin::Feature,
143        rule: Option<&gherkin::Rule>,
144        scenario: &gherkin::Scenario,
145        cli: &Cli,
146    ) -> Option<Self> {
147        let parse_tags = |tags: &[String]| {
148            tags.iter().find_map(|tag| {
149                tag.strip_prefix("retry").map(|retries| {
150                    let (num, rest) = retries
151                        .strip_prefix('(')
152                        .and_then(|s| {
153                            let (num, rest) = s.split_once(')')?;
154                            num.parse::<usize>()
155                                .ok()
156                                .map(|num| (Some(num), rest))
157                        })
158                        .unwrap_or((None, retries));
159
160                    let after = rest.strip_prefix(".after").and_then(|after| {
161                        let after = after.strip_prefix('(')?;
162                        let (dur, _) = after.split_once(')')?;
163                        humantime::parse_duration(dur).ok()
164                    });
165
166                    (num, after)
167                })
168            })
169        };
170
171        let apply_cli = |options: Option<_>| {
172            let matched = cli.retry_tag_filter.as_ref().map_or_else(
173                || cli.retry.is_some() || cli.retry_after.is_some(),
174                |op| {
175                    op.eval(scenario.tags.iter().chain(
176                        rule.iter().flat_map(|r| &r.tags).chain(&feature.tags),
177                    ))
178                },
179            );
180
181            (options.is_some() || matched).then(|| Self {
182                retries: Retries::initial(
183                    options.and_then(|(r, _)| r).or(cli.retry).unwrap_or(1),
184                ),
185                after: options.and_then(|(_, a)| a).or(cli.retry_after),
186            })
187        };
188
189        apply_cli(
190            parse_tags(&scenario.tags)
191                .or_else(|| parse_tags(&rule?.tags))
192                .or_else(|| parse_tags(&feature.tags)),
193        )
194    }
195
196    /// Constructs [`RetryOptionsWithDeadline`], that will reschedule
197    /// [`Scenario`] [`after`] delay.
198    ///
199    /// [`after`]: RetryOptions::after
200    /// [`Scenario`]: gherkin::Scenario
201    fn with_deadline(self, now: Instant) -> RetryOptionsWithDeadline {
202        RetryOptionsWithDeadline {
203            retries: self.retries,
204            after: self.after.map(|at| (at, Some(now))),
205        }
206    }
207
208    /// Constructs [`RetryOptionsWithDeadline`], that will reschedule
209    /// [`Scenario`] immediately, ignoring [`RetryOptions::after`]. Used for
210    /// initial [`Scenario`] run, where we don't need to wait for the delay.
211    ///
212    /// [`Scenario`]: gherkin::Scenario
213    fn without_deadline(self) -> RetryOptionsWithDeadline {
214        RetryOptionsWithDeadline {
215            retries: self.retries,
216            after: self.after.map(|at| (at, None)),
217        }
218    }
219}
220
221/// [`RetryOptions`] with an [`Option`]al [`Instant`] to determine, whether
222/// [`Scenario`] should be already rescheduled or not.
223///
224/// [`Scenario`]: gherkin::Scenario
225#[derive(Clone, Copy, Debug)]
226pub struct RetryOptionsWithDeadline {
227    /// Number of [`Retries`].
228    pub retries: Retries,
229
230    /// Delay before next retry attempt will be executed.
231    pub after: Option<(Duration, Option<Instant>)>,
232}
233
234impl From<RetryOptionsWithDeadline> for RetryOptions {
235    fn from(v: RetryOptionsWithDeadline) -> Self {
236        Self { retries: v.retries, after: v.after.map(|(at, _)| at) }
237    }
238}
239
240impl RetryOptionsWithDeadline {
241    /// Returns [`Duration`] after which a [`Scenario`] could be retried. If
242    /// [`None`], then [`Scenario`] is ready for the retry.
243    ///
244    /// [`Scenario`]: gherkin::Scenario
245    fn left_until_retry(&self) -> Option<Duration> {
246        let (dur, instant) = self.after?;
247        dur.checked_sub(instant?.elapsed())
248    }
249}
250
251/// Alias for [`fn`] used to determine whether a [`Scenario`] is [`Concurrent`]
252/// or a [`Serial`] one.
253///
254/// [`Concurrent`]: ScenarioType::Concurrent
255/// [`Serial`]: ScenarioType::Serial
256/// [`Scenario`]: gherkin::Scenario
257pub type WhichScenarioFn = fn(
258    &gherkin::Feature,
259    Option<&gherkin::Rule>,
260    &gherkin::Scenario,
261) -> ScenarioType;
262
263/// Alias for [`Arc`]ed [`Fn`] used to determine [`Scenario`]'s
264/// [`RetryOptions`].
265///
266/// [`Scenario`]: gherkin::Scenario
267pub type RetryOptionsFn = Arc<
268    dyn Fn(
269        &gherkin::Feature,
270        Option<&gherkin::Rule>,
271        &gherkin::Scenario,
272        &Cli,
273    ) -> Option<RetryOptions>,
274>;
275
276/// Alias for [`fn`] executed on each [`Scenario`] before running all [`Step`]s.
277///
278/// [`Scenario`]: gherkin::Scenario
279/// [`Step`]: gherkin::Step
280pub type BeforeHookFn<World> = for<'a> fn(
281    &'a gherkin::Feature,
282    Option<&'a gherkin::Rule>,
283    &'a gherkin::Scenario,
284    &'a mut World,
285) -> LocalBoxFuture<'a, ()>;
286
287/// Alias for [`fn`] executed on each [`Scenario`] after running all [`Step`]s.
288///
289/// [`Scenario`]: gherkin::Scenario
290/// [`Step`]: gherkin::Step
291pub type AfterHookFn<World> = for<'a> fn(
292    &'a gherkin::Feature,
293    Option<&'a gherkin::Rule>,
294    &'a gherkin::Scenario,
295    &'a event::ScenarioFinished,
296    Option<&'a mut World>,
297) -> LocalBoxFuture<'a, ()>;
298
299/// Alias for a failed [`Scenario`].
300///
301/// [`Scenario`]: gherkin::Scenario
302type IsFailed = bool;
303
304/// Alias for a retried [`Scenario`].
305///
306/// [`Scenario`]: gherkin::Scenario
307type IsRetried = bool;
308
309/// Default [`Runner`] implementation which follows [_order guarantees_][1] from
310/// the [`Runner`] trait docs.
311///
312/// Executes [`Scenario`]s concurrently based on the custom function, which
313/// returns [`ScenarioType`]. Also, can limit maximum number of concurrent
314/// [`Scenario`]s.
315///
316/// [1]: Runner#order-guarantees
317/// [`Scenario`]: gherkin::Scenario
318#[derive(Debug)]
319pub struct Basic<
320    World,
321    F = WhichScenarioFn,
322    Before = BeforeHookFn<World>,
323    After = AfterHookFn<World>,
324> {
325    /// Optional number of concurrently executed [`Scenario`]s.
326    ///
327    /// [`Scenario`]: gherkin::Scenario
328    max_concurrent_scenarios: Option<usize>,
329
330    /// Optional number of retries of failed [`Scenario`]s.
331    ///
332    /// [`Scenario`]: gherkin::Scenario
333    retries: Option<usize>,
334
335    /// Optional [`Duration`] between retries of failed [`Scenario`]s.
336    ///
337    /// [`Scenario`]: gherkin::Scenario
338    retry_after: Option<Duration>,
339
340    /// Optional [`TagOperation`] filter for retries of failed [`Scenario`]s.
341    ///
342    /// [`Scenario`]: gherkin::Scenario
343    retry_filter: Option<TagOperation>,
344
345    /// [`Collection`] of functions to match [`Step`]s.
346    ///
347    /// [`Collection`]: step::Collection
348    steps: step::Collection<World>,
349
350    /// Function determining whether a [`Scenario`] is [`Concurrent`] or
351    /// a [`Serial`] one.
352    ///
353    /// [`Concurrent`]: ScenarioType::Concurrent
354    /// [`Serial`]: ScenarioType::Serial
355    /// [`Scenario`]: gherkin::Scenario
356    #[debug(ignore)]
357    which_scenario: F,
358
359    /// Function determining [`Scenario`]'s [`RetryOptions`].
360    ///
361    /// [`Scenario`]: gherkin::Scenario
362    #[debug(ignore)]
363    retry_options: RetryOptionsFn,
364
365    /// Function, executed on each [`Scenario`] before running all [`Step`]s,
366    /// including [`Background`] ones.
367    ///
368    /// [`Background`]: gherkin::Background
369    /// [`Scenario`]: gherkin::Scenario
370    /// [`Step`]: gherkin::Step
371    #[debug(ignore)]
372    before_hook: Option<Before>,
373
374    /// Function, executed on each [`Scenario`] after running all [`Step`]s.
375    ///
376    /// [`Background`]: gherkin::Background
377    /// [`Scenario`]: gherkin::Scenario
378    /// [`Step`]: gherkin::Step
379    #[debug(ignore)]
380    after_hook: Option<After>,
381
382    /// Indicates whether execution should be stopped after the first failure.
383    fail_fast: bool,
384
385    #[cfg(feature = "tracing")]
386    /// [`TracingCollector`] for [`event::Scenario::Log`]s forwarding.
387    #[debug(ignore)]
388    pub(crate) logs_collector: Arc<AtomicCell<Box<Option<TracingCollector>>>>,
389}
390
391#[cfg(feature = "tracing")]
392/// Assertion that [`Basic::logs_collector`] [`AtomicCell::is_lock_free`].
393const _: () = {
394    assert!(
395        AtomicCell::<Box<Option<TracingCollector>>>::is_lock_free(),
396        "`AtomicCell::<Box<Option<TracingCollector>>>` is not lock-free",
397    );
398};
399
400// Implemented manually to omit redundant `World: Clone` trait bound, imposed by
401// `#[derive(Clone)]`.
402impl<World, F: Clone, B: Clone, A: Clone> Clone for Basic<World, F, B, A> {
403    fn clone(&self) -> Self {
404        Self {
405            max_concurrent_scenarios: self.max_concurrent_scenarios,
406            retries: self.retries,
407            retry_after: self.retry_after,
408            retry_filter: self.retry_filter.clone(),
409            steps: self.steps.clone(),
410            which_scenario: self.which_scenario.clone(),
411            retry_options: Arc::clone(&self.retry_options),
412            before_hook: self.before_hook.clone(),
413            after_hook: self.after_hook.clone(),
414            fail_fast: self.fail_fast,
415            #[cfg(feature = "tracing")]
416            logs_collector: Arc::clone(&self.logs_collector),
417        }
418    }
419}
420
421impl<World> Default for Basic<World> {
422    fn default() -> Self {
423        let which_scenario: WhichScenarioFn = |feature, rule, scenario| {
424            scenario
425                .tags
426                .iter()
427                .chain(rule.iter().flat_map(|r| &r.tags))
428                .chain(&feature.tags)
429                .find(|tag| *tag == "serial")
430                .map_or(ScenarioType::Concurrent, |_| ScenarioType::Serial)
431        };
432
433        Self {
434            max_concurrent_scenarios: Some(64),
435            retries: None,
436            retry_after: None,
437            retry_filter: None,
438            steps: step::Collection::new(),
439            which_scenario,
440            retry_options: Arc::new(RetryOptions::parse_from_tags),
441            before_hook: None,
442            after_hook: None,
443            fail_fast: false,
444            #[cfg(feature = "tracing")]
445            logs_collector: Arc::new(AtomicCell::new(Box::new(None))),
446        }
447    }
448}
449
450impl<World, Which, Before, After> Basic<World, Which, Before, After> {
451    /// If `max` is [`Some`], then number of concurrently executed [`Scenario`]s
452    /// will be limited.
453    ///
454    /// [`Scenario`]: gherkin::Scenario
455    #[must_use]
456    pub fn max_concurrent_scenarios(
457        mut self,
458        max: impl Into<Option<usize>>,
459    ) -> Self {
460        self.max_concurrent_scenarios = max.into();
461        self
462    }
463
464    /// If `retries` is [`Some`], then failed [`Scenario`]s will be retried
465    /// specified number of times.
466    ///
467    /// [`Scenario`]: gherkin::Scenario
468    #[must_use]
469    pub fn retries(mut self, retries: impl Into<Option<usize>>) -> Self {
470        self.retries = retries.into();
471        self
472    }
473
474    /// If `after` is [`Some`], then failed [`Scenario`]s will be retried after
475    /// the specified [`Duration`].
476    ///
477    /// [`Scenario`]: gherkin::Scenario
478    #[must_use]
479    pub fn retry_after(mut self, after: impl Into<Option<Duration>>) -> Self {
480        self.retry_after = after.into();
481        self
482    }
483
484    /// If `filter` is [`Some`], then failed [`Scenario`]s will be retried only
485    /// if they're matching the specified `tag_expression`.
486    ///
487    /// [`Scenario`]: gherkin::Scenario
488    #[must_use]
489    pub fn retry_filter(
490        mut self,
491        tag_expression: impl Into<Option<TagOperation>>,
492    ) -> Self {
493        self.retry_filter = tag_expression.into();
494        self
495    }
496
497    /// Makes stop running tests on the first failure.
498    ///
499    /// __NOTE__: All the already started [`Scenario`]s at the moment of failure
500    ///           will be finished.
501    ///
502    /// __NOTE__: Retried [`Scenario`]s are considered as failed, only in case
503    ///           they exhaust all retry attempts and still fail.
504    ///
505    /// [`Scenario`]: gherkin::Scenario
506    #[must_use]
507    pub const fn fail_fast(mut self) -> Self {
508        self.fail_fast = true;
509        self
510    }
511
512    /// Function determining whether a [`Scenario`] is [`Concurrent`] or
513    /// a [`Serial`] one.
514    ///
515    /// [`Concurrent`]: ScenarioType::Concurrent
516    /// [`Serial`]: ScenarioType::Serial
517    /// [`Scenario`]: gherkin::Scenario
518    #[must_use]
519    pub fn which_scenario<F>(self, func: F) -> Basic<World, F, Before, After>
520    where
521        F: Fn(
522                &gherkin::Feature,
523                Option<&gherkin::Rule>,
524                &gherkin::Scenario,
525            ) -> ScenarioType
526            + 'static,
527    {
528        let Self {
529            max_concurrent_scenarios,
530            retries,
531            retry_after,
532            retry_filter,
533            steps,
534            retry_options,
535            before_hook,
536            after_hook,
537            fail_fast,
538            #[cfg(feature = "tracing")]
539            logs_collector,
540            ..
541        } = self;
542        Basic {
543            max_concurrent_scenarios,
544            retries,
545            retry_after,
546            retry_filter,
547            steps,
548            which_scenario: func,
549            retry_options,
550            before_hook,
551            after_hook,
552            fail_fast,
553            #[cfg(feature = "tracing")]
554            logs_collector,
555        }
556    }
557
558    /// Function determining [`Scenario`]'s [`RetryOptions`].
559    ///
560    /// [`Scenario`]: gherkin::Scenario
561    #[must_use]
562    pub fn retry_options<R>(mut self, func: R) -> Self
563    where
564        R: Fn(
565                &gherkin::Feature,
566                Option<&gherkin::Rule>,
567                &gherkin::Scenario,
568                &Cli,
569            ) -> Option<RetryOptions>
570            + 'static,
571    {
572        self.retry_options = Arc::new(func);
573        self
574    }
575
576    /// Sets a hook, executed on each [`Scenario`] before running all its
577    /// [`Step`]s, including [`Background`] ones.
578    ///
579    /// > **NOTE**: Only one [`before`] hook can be registered. If
580    /// >           multiple calls are made, only the last one will be
581    /// >           run.
582    ///
583    /// [`before`]: Self::before()
584    /// [`Background`]: gherkin::Background
585    /// [`Scenario`]: gherkin::Scenario
586    /// [`Step`]: gherkin::Step
587    #[must_use]
588    pub fn before<Func>(self, func: Func) -> Basic<World, Which, Func, After>
589    where
590        Func: for<'a> Fn(
591            &'a gherkin::Feature,
592            Option<&'a gherkin::Rule>,
593            &'a gherkin::Scenario,
594            &'a mut World,
595        ) -> LocalBoxFuture<'a, ()>,
596    {
597        let Self {
598            max_concurrent_scenarios,
599            retries,
600            retry_after,
601            retry_filter,
602            steps,
603            which_scenario,
604            retry_options,
605            after_hook,
606            fail_fast,
607            #[cfg(feature = "tracing")]
608            logs_collector,
609            ..
610        } = self;
611        Basic {
612            max_concurrent_scenarios,
613            retries,
614            retry_after,
615            retry_filter,
616            steps,
617            which_scenario,
618            retry_options,
619            before_hook: Some(func),
620            after_hook,
621            fail_fast,
622            #[cfg(feature = "tracing")]
623            logs_collector,
624        }
625    }
626
627    /// Sets hook, executed on each [`Scenario`] after running all its
628    /// [`Step`]s, even after [`Skipped`] of [`Failed`] ones.
629    ///
630    /// > **NOTE**: Only one [`after`] hook can be registered. If
631    /// >           multiple calls are made, only the last one will be
632    /// >           run.
633    ///
634    /// Last `World` argument is supplied to the function, in case it was
635    /// initialized before by running [`before`] hook or any [`Step`].
636    ///
637    /// [`after`]: Self::after()
638    /// [`before`]: Self::before()
639    /// [`Failed`]: event::Step::Failed
640    /// [`Scenario`]: gherkin::Scenario
641    /// [`Skipped`]: event::Step::Skipped
642    /// [`Step`]: gherkin::Step
643    #[must_use]
644    pub fn after<Func>(self, func: Func) -> Basic<World, Which, Before, Func>
645    where
646        Func: for<'a> Fn(
647            &'a gherkin::Feature,
648            Option<&'a gherkin::Rule>,
649            &'a gherkin::Scenario,
650            &'a event::ScenarioFinished,
651            Option<&'a mut World>,
652        ) -> LocalBoxFuture<'a, ()>,
653    {
654        let Self {
655            max_concurrent_scenarios,
656            retries,
657            retry_after,
658            retry_filter,
659            steps,
660            which_scenario,
661            retry_options,
662            before_hook,
663            fail_fast,
664            #[cfg(feature = "tracing")]
665            logs_collector,
666            ..
667        } = self;
668        Basic {
669            max_concurrent_scenarios,
670            retries,
671            retry_after,
672            retry_filter,
673            steps,
674            which_scenario,
675            retry_options,
676            before_hook,
677            after_hook: Some(func),
678            fail_fast,
679            #[cfg(feature = "tracing")]
680            logs_collector,
681        }
682    }
683
684    /// Sets the given [`Collection`] of [`Step`]s to this [`Runner`].
685    ///
686    /// [`Collection`]: step::Collection
687    #[must_use]
688    pub fn steps(mut self, steps: step::Collection<World>) -> Self {
689        self.steps = steps;
690        self
691    }
692
693    /// Adds a [Given] [`Step`] matching the given `regex`.
694    ///
695    /// [Given]: https://cucumber.io/docs/gherkin/reference#given
696    #[must_use]
697    pub fn given(mut self, regex: Regex, step: Step<World>) -> Self {
698        self.steps = mem::take(&mut self.steps).given(None, regex, step);
699        self
700    }
701
702    /// Adds a [When] [`Step`] matching the given `regex`.
703    ///
704    /// [When]: https://cucumber.io/docs/gherkin/reference#given
705    #[must_use]
706    pub fn when(mut self, regex: Regex, step: Step<World>) -> Self {
707        self.steps = mem::take(&mut self.steps).when(None, regex, step);
708        self
709    }
710
711    /// Adds a [Then] [`Step`] matching the given `regex`.
712    ///
713    /// [Then]: https://cucumber.io/docs/gherkin/reference#then
714    #[must_use]
715    pub fn then(mut self, regex: Regex, step: Step<World>) -> Self {
716        self.steps = mem::take(&mut self.steps).then(None, regex, step);
717        self
718    }
719}
720
721impl<W, Which, Before, After> Runner<W> for Basic<W, Which, Before, After>
722where
723    W: World,
724    Which: Fn(
725            &gherkin::Feature,
726            Option<&gherkin::Rule>,
727            &gherkin::Scenario,
728        ) -> ScenarioType
729        + 'static,
730    Before: for<'a> Fn(
731            &'a gherkin::Feature,
732            Option<&'a gherkin::Rule>,
733            &'a gherkin::Scenario,
734            &'a mut W,
735        ) -> LocalBoxFuture<'a, ()>
736        + 'static,
737    After: for<'a> Fn(
738            &'a gherkin::Feature,
739            Option<&'a gherkin::Rule>,
740            &'a gherkin::Scenario,
741            &'a event::ScenarioFinished,
742            Option<&'a mut W>,
743        ) -> LocalBoxFuture<'a, ()>
744        + 'static,
745{
746    type Cli = Cli;
747
748    type EventStream =
749        LocalBoxStream<'static, parser::Result<Event<event::Cucumber<W>>>>;
750
751    fn run<S>(self, features: S, mut cli: Cli) -> Self::EventStream
752    where
753        S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
754    {
755        #[cfg(feature = "tracing")]
756        let logs_collector = *self.logs_collector.swap(Box::new(None));
757        let Self {
758            max_concurrent_scenarios,
759            retries,
760            retry_after,
761            retry_filter,
762            steps,
763            which_scenario,
764            retry_options,
765            before_hook,
766            after_hook,
767            fail_fast,
768            ..
769        } = self;
770
771        cli.retry = cli.retry.or(retries);
772        cli.retry_after = cli.retry_after.or(retry_after);
773        cli.retry_tag_filter = cli.retry_tag_filter.or(retry_filter);
774        let fail_fast = cli.fail_fast || fail_fast;
775        let concurrency = cli.concurrency.or(max_concurrent_scenarios);
776
777        let buffer = Features::default();
778        let (sender, receiver) = mpsc::unbounded();
779
780        let insert = insert_features(
781            buffer.clone(),
782            features,
783            which_scenario,
784            retry_options,
785            sender.clone(),
786            cli,
787            fail_fast,
788        );
789        let execute = execute(
790            buffer,
791            concurrency,
792            steps,
793            sender,
794            before_hook,
795            after_hook,
796            fail_fast,
797            #[cfg(feature = "tracing")]
798            logs_collector,
799        );
800
801        stream::select(
802            receiver.map(Either::Left),
803            future::join(insert, execute).into_stream().map(Either::Right),
804        )
805        .filter_map(async |r| match r {
806            Either::Left(ev) => Some(ev),
807            Either::Right(_) => None,
808        })
809        .boxed_local()
810    }
811}
812
813/// Stores [`Feature`]s for later use by [`execute()`].
814///
815/// [`Feature`]: gherkin::Feature
816async fn insert_features<W, S, F>(
817    into: Features,
818    features_stream: S,
819    which_scenario: F,
820    retries: RetryOptionsFn,
821    sender: mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,
822    cli: Cli,
823    fail_fast: bool,
824) where
825    S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
826    F: Fn(
827            &gherkin::Feature,
828            Option<&gherkin::Rule>,
829            &gherkin::Scenario,
830        ) -> ScenarioType
831        + 'static,
832{
833    let mut features = 0;
834    let mut rules = 0;
835    let mut scenarios = 0;
836    let mut steps = 0;
837    let mut parser_errors = 0;
838
839    pin_mut!(features_stream);
840    while let Some(feat) = features_stream.next().await {
841        match feat {
842            Ok(f) => {
843                features += 1;
844                rules += f.rules.len();
845                scenarios += f.count_scenarios();
846                steps += f.count_steps();
847
848                into.insert(f, &which_scenario, &retries, &cli).await;
849            }
850            Err(e) => {
851                parser_errors += 1;
852
853                // If the receiver end is dropped, then no one listens for the
854                // events, so we can just stop from here.
855                if sender.unbounded_send(Err(e)).is_err() || fail_fast {
856                    break;
857                }
858            }
859        }
860    }
861
862    drop(sender.unbounded_send(Ok(Event::new(
863        event::Cucumber::ParsingFinished {
864            features,
865            rules,
866            scenarios,
867            steps,
868            parser_errors,
869        },
870    ))));
871
872    into.finish();
873}
874
875/// Retrieves [`Feature`]s and executes them.
876///
877/// # Events
878///
879/// - [`Scenario`] events are emitted by [`Executor`].
880/// - If [`Scenario`] was first or last for particular [`Rule`] or [`Feature`],
881///   emits starting or finishing events for them.
882///
883/// [`Feature`]: gherkin::Feature
884/// [`Rule`]: gherkin::Rule
885/// [`Scenario`]: gherkin::Scenario
886// TODO: Needs refactoring.
887#[expect(clippy::too_many_lines, reason = "needs refactoring")]
888#[cfg_attr(
889    feature = "tracing",
890    expect(clippy::too_many_arguments, reason = "needs refactoring")
891)]
892async fn execute<W, Before, After>(
893    features: Features,
894    max_concurrent_scenarios: Option<usize>,
895    collection: step::Collection<W>,
896    event_sender: mpsc::UnboundedSender<
897        parser::Result<Event<event::Cucumber<W>>>,
898    >,
899    before_hook: Option<Before>,
900    after_hook: Option<After>,
901    fail_fast: bool,
902    #[cfg(feature = "tracing")] mut logs_collector: Option<TracingCollector>,
903) where
904    W: World,
905    Before: 'static
906        + for<'a> Fn(
907            &'a gherkin::Feature,
908            Option<&'a gherkin::Rule>,
909            &'a gherkin::Scenario,
910            &'a mut W,
911        ) -> LocalBoxFuture<'a, ()>,
912    After: 'static
913        + for<'a> Fn(
914            &'a gherkin::Feature,
915            Option<&'a gherkin::Rule>,
916            &'a gherkin::Scenario,
917            &'a event::ScenarioFinished,
918            Option<&'a mut W>,
919        ) -> LocalBoxFuture<'a, ()>,
920{
921    // Those panic hook shenanigans are done to avoid console messages like
922    // "thread 'main' panicked at ..."
923    //
924    // 1. We obtain the current panic hook and replace it with an empty one.
925    // 2. We run tests, which can panic. In that case we pass all panic info
926    //    down the line to the Writer, which will print it at a right time.
927    // 3. We restore original panic hook, because suppressing all panics doesn't
928    //    sound like a very good idea.
929    let hook = panic::take_hook();
930    panic::set_hook(Box::new(|_| {}));
931
932    let (finished_sender, finished_receiver) = mpsc::unbounded();
933    let mut storage = FinishedRulesAndFeatures::new(finished_receiver);
934    let executor = Executor::new(
935        collection,
936        before_hook,
937        after_hook,
938        event_sender,
939        finished_sender,
940        features.clone(),
941    );
942
943    executor.send_event(event::Cucumber::Started);
944
945    #[cfg(feature = "tracing")]
946    let waiter = logs_collector
947        .as_ref()
948        .map(TracingCollector::scenario_span_event_waiter);
949
950    let mut started_scenarios = ControlFlow::Continue(max_concurrent_scenarios);
951    let mut run_scenarios = stream::FuturesUnordered::new();
952    loop {
953        let (runnable, sleep) = features
954            .get(started_scenarios.continue_value().unwrap_or(Some(0)))
955            .await;
956        if run_scenarios.is_empty() && runnable.is_empty() {
957            if features.is_finished(started_scenarios.is_break()).await {
958                break;
959            }
960
961            // To avoid busy-polling of `Features::get()`, in case there are no
962            // scenarios that are running or scheduled for execution, we spawn a
963            // thread, that sleeps for minimal deadline of all retried
964            // scenarios.
965            // TODO: Replace `thread::spawn` with async runtime agnostic sleep,
966            //       once it's available.
967            if let Some(dur) = sleep {
968                let (sender, receiver) = oneshot::channel();
969                drop(thread::spawn(move || {
970                    thread::sleep(dur);
971                    sender.send(())
972                }));
973                _ = receiver.await.ok();
974            }
975
976            continue;
977        }
978
979        let started = storage.start_scenarios(&runnable);
980        executor.send_all_events(started);
981
982        {
983            #[cfg(feature = "tracing")]
984            let forward_logs = {
985                if let Some(coll) = logs_collector.as_mut() {
986                    coll.start_scenarios(&runnable);
987                }
988                async {
989                    loop {
990                        while let Some(logs) = logs_collector
991                            .as_mut()
992                            .and_then(TracingCollector::emitted_logs)
993                        {
994                            executor.send_all_events(logs);
995                        }
996                        future::ready(()).then_yield().await;
997                    }
998                }
999            };
1000            #[cfg(feature = "tracing")]
1001            pin_mut!(forward_logs);
1002            #[cfg(not(feature = "tracing"))]
1003            let forward_logs = future::pending();
1004
1005            if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios {
1006                *sc -= runnable.len();
1007            }
1008
1009            for (id, f, r, s, ty, retries) in runnable {
1010                run_scenarios.push(
1011                    executor
1012                        .run_scenario(
1013                            id,
1014                            f,
1015                            r,
1016                            s,
1017                            ty,
1018                            retries,
1019                            #[cfg(feature = "tracing")]
1020                            waiter.as_ref(),
1021                        )
1022                        .then_yield(),
1023                );
1024            }
1025
1026            let (finished_scenario, _) =
1027                select_with_biased_first(forward_logs, run_scenarios.next())
1028                    .await
1029                    .factor_first();
1030            if finished_scenario.is_some()
1031                && let ControlFlow::Continue(Some(sc)) = &mut started_scenarios
1032            {
1033                *sc += 1;
1034            }
1035        }
1036
1037        while let Ok((id, feat, rule, scenario_failed, retried)) =
1038            storage.finished_receiver.try_recv()
1039        {
1040            if let Some(rule) = rule
1041                && let Some(f) =
1042                    storage.rule_scenario_finished(feat.clone(), rule, retried)
1043            {
1044                executor.send_event(f);
1045            }
1046            if let Some(f) = storage.feature_scenario_finished(feat, retried) {
1047                executor.send_event(f);
1048            }
1049            #[cfg(feature = "tracing")]
1050            {
1051                if let Some(coll) = logs_collector.as_mut() {
1052                    coll.finish_scenario(id);
1053                }
1054            }
1055            #[cfg(not(feature = "tracing"))]
1056            let _: ScenarioId = id;
1057
1058            if fail_fast && scenario_failed && !retried {
1059                started_scenarios = ControlFlow::Break(());
1060            }
1061        }
1062    }
1063
1064    // This is done in case of `fail_fast: true`, when not all `Scenario`s might
1065    // be executed.
1066    executor.send_all_events(storage.finish_all_rules_and_features());
1067
1068    executor.send_event(event::Cucumber::Finished);
1069
1070    panic::set_hook(hook);
1071}
1072
1073/// Runs [`Scenario`]s and notifies about their state of completion.
1074///
1075/// [`Scenario`]: gherkin::Scenario
1076struct Executor<W, Before, After> {
1077    /// [`Step`]s [`Collection`].
1078    ///
1079    /// [`Collection`]: step::Collection
1080    collection: step::Collection<W>,
1081
1082    /// Function, executed on each [`Scenario`] before running all [`Step`]s,
1083    /// including [`Background`] ones.
1084    ///
1085    /// [`Background`]: gherkin::Background
1086    /// [`Scenario`]: gherkin::Scenario
1087    /// [`Step`]: gherkin::Step
1088    before_hook: Option<Before>,
1089
1090    /// Function, executed on each [`Scenario`] after running all [`Step`]s.
1091    ///
1092    /// [`Scenario`]: gherkin::Scenario
1093    /// [`Step`]: gherkin::Step
1094    after_hook: Option<After>,
1095
1096    /// Sender for [`Scenario`] [events][1].
1097    ///
1098    /// [`Scenario`]: gherkin::Scenario
1099    /// [1]: event::Scenario
1100    event_sender:
1101        mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,
1102
1103    /// Sender for notifying of [`Scenario`]s completion.
1104    ///
1105    /// [`Scenario`]: gherkin::Scenario
1106    finished_sender: FinishedFeaturesSender,
1107
1108    /// [`Scenario`]s storage.
1109    ///
1110    /// [`Scenario`]: gherkin::Scenario
1111    storage: Features,
1112}
1113
1114impl<W: World, Before, After> Executor<W, Before, After>
1115where
1116    Before: 'static
1117        + for<'a> Fn(
1118            &'a gherkin::Feature,
1119            Option<&'a gherkin::Rule>,
1120            &'a gherkin::Scenario,
1121            &'a mut W,
1122        ) -> LocalBoxFuture<'a, ()>,
1123    After: 'static
1124        + for<'a> Fn(
1125            &'a gherkin::Feature,
1126            Option<&'a gherkin::Rule>,
1127            &'a gherkin::Scenario,
1128            &'a event::ScenarioFinished,
1129            Option<&'a mut W>,
1130        ) -> LocalBoxFuture<'a, ()>,
1131{
1132    /// Creates a new [`Executor`].
1133    const fn new(
1134        collection: step::Collection<W>,
1135        before_hook: Option<Before>,
1136        after_hook: Option<After>,
1137        event_sender: mpsc::UnboundedSender<
1138            parser::Result<Event<event::Cucumber<W>>>,
1139        >,
1140        finished_sender: FinishedFeaturesSender,
1141        storage: Features,
1142    ) -> Self {
1143        Self {
1144            collection,
1145            before_hook,
1146            after_hook,
1147            event_sender,
1148            finished_sender,
1149            storage,
1150        }
1151    }
1152
1153    /// Runs a [`Scenario`].
1154    ///
1155    /// # Events
1156    ///
1157    /// - Emits all [`Scenario`] events.
1158    ///
1159    /// [`Feature`]: gherkin::Feature
1160    /// [`Rule`]: gherkin::Rule
1161    /// [`Scenario`]: gherkin::Scenario
1162    // TODO: Needs refactoring.
1163    #[expect(clippy::too_many_lines, reason = "needs refactoring")]
1164    #[cfg_attr(
1165        feature = "tracing",
1166        expect(clippy::too_many_arguments, reason = "needs refactoring")
1167    )]
1168    async fn run_scenario(
1169        &self,
1170        id: ScenarioId,
1171        feature: Source<gherkin::Feature>,
1172        rule: Option<Source<gherkin::Rule>>,
1173        scenario: Source<gherkin::Scenario>,
1174        scenario_ty: ScenarioType,
1175        retries: Option<RetryOptions>,
1176        #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1177    ) {
1178        let retry_num = retries.map(|r| r.retries);
1179        let ok = |e: fn(_) -> event::Scenario<W>| {
1180            let (f, r, s) = (&feature, &rule, &scenario);
1181            move |step| {
1182                let (f, r, s) = (f.clone(), r.clone(), s.clone());
1183                let event = e(step).with_retries(retry_num);
1184                event::Cucumber::scenario(f, r, s, event)
1185            }
1186        };
1187        let ok_capt = |e: fn(_, _, _) -> event::Scenario<W>| {
1188            let (f, r, s) = (&feature, &rule, &scenario);
1189            move |step, cap, loc| {
1190                let (f, r, s) = (f.clone(), r.clone(), s.clone());
1191                let event = e(step, cap, loc).with_retries(retry_num);
1192                event::Cucumber::scenario(f, r, s, event)
1193            }
1194        };
1195
1196        let compose = |started, passed, skipped| {
1197            (ok(started), ok_capt(passed), ok(skipped))
1198        };
1199        let into_bg_step_ev = compose(
1200            event::Scenario::background_step_started,
1201            event::Scenario::background_step_passed,
1202            event::Scenario::background_step_skipped,
1203        );
1204        let into_step_ev = compose(
1205            event::Scenario::step_started,
1206            event::Scenario::step_passed,
1207            event::Scenario::step_skipped,
1208        );
1209
1210        self.send_event(event::Cucumber::scenario(
1211            feature.clone(),
1212            rule.clone(),
1213            scenario.clone(),
1214            event::Scenario::Started.with_retries(retry_num),
1215        ));
1216
1217        let is_failed = async {
1218            let mut result = async {
1219                let before_hook = self
1220                    .run_before_hook(
1221                        &feature,
1222                        rule.as_ref(),
1223                        &scenario,
1224                        retry_num,
1225                        id,
1226                        #[cfg(feature = "tracing")]
1227                        waiter,
1228                    )
1229                    .await?;
1230
1231                let feature_background = feature
1232                    .background
1233                    .as_ref()
1234                    .map(|b| b.steps.iter().map(|s| Source::new(s.clone())))
1235                    .into_iter()
1236                    .flatten();
1237
1238                let feature_background = stream::iter(feature_background)
1239                    .map(Ok)
1240                    .try_fold(before_hook, |world, bg_step| {
1241                        self.run_step(
1242                            world,
1243                            bg_step,
1244                            true,
1245                            into_bg_step_ev,
1246                            id,
1247                            #[cfg(feature = "tracing")]
1248                            waiter,
1249                        )
1250                        .map_ok(Some)
1251                    })
1252                    .await?;
1253
1254                let rule_background = rule
1255                    .as_ref()
1256                    .map(|r| {
1257                        r.background
1258                            .as_ref()
1259                            .map(|b| {
1260                                b.steps.iter().map(|s| Source::new(s.clone()))
1261                            })
1262                            .into_iter()
1263                            .flatten()
1264                    })
1265                    .into_iter()
1266                    .flatten();
1267
1268                let rule_background = stream::iter(rule_background)
1269                    .map(Ok)
1270                    .try_fold(feature_background, |world, bg_step| {
1271                        self.run_step(
1272                            world,
1273                            bg_step,
1274                            true,
1275                            into_bg_step_ev,
1276                            id,
1277                            #[cfg(feature = "tracing")]
1278                            waiter,
1279                        )
1280                        .map_ok(Some)
1281                    })
1282                    .await?;
1283
1284                stream::iter(
1285                    scenario.steps.iter().map(|s| Source::new(s.clone())),
1286                )
1287                .map(Ok)
1288                .try_fold(rule_background, |world, step| {
1289                    self.run_step(
1290                        world,
1291                        step,
1292                        false,
1293                        into_step_ev,
1294                        id,
1295                        #[cfg(feature = "tracing")]
1296                        waiter,
1297                    )
1298                    .map_ok(Some)
1299                })
1300                .await
1301            }
1302            .await;
1303
1304            let (world, scenario_finished_ev) = match &mut result {
1305                Ok(world) => {
1306                    (world.take(), event::ScenarioFinished::StepPassed)
1307                }
1308                Err(exec_err) => (
1309                    exec_err.take_world(),
1310                    exec_err.get_scenario_finished_event(),
1311                ),
1312            };
1313
1314            let (world, after_hook_meta, after_hook_error) = self
1315                .run_after_hook(
1316                    world,
1317                    &feature,
1318                    rule.as_ref(),
1319                    &scenario,
1320                    scenario_finished_ev,
1321                    id,
1322                    #[cfg(feature = "tracing")]
1323                    waiter,
1324                )
1325                .await
1326                .map_or_else(
1327                    |(w, meta, info)| (w.map(Arc::new), Some(meta), Some(info)),
1328                    |(w, meta)| (w.map(Arc::new), meta, None),
1329                );
1330
1331            let scenario_failed = match &result {
1332                Ok(_) | Err(ExecutionFailure::StepSkipped(_)) => false,
1333                Err(
1334                    ExecutionFailure::BeforeHookPanicked { .. }
1335                    | ExecutionFailure::StepPanicked { .. },
1336                ) => true,
1337            };
1338            let is_failed = scenario_failed || after_hook_error.is_some();
1339
1340            if let Some(exec_error) = result.err() {
1341                self.emit_failed_events(
1342                    feature.clone(),
1343                    rule.clone(),
1344                    scenario.clone(),
1345                    world.clone(),
1346                    exec_error,
1347                    retry_num,
1348                );
1349            }
1350
1351            self.emit_after_hook_events(
1352                feature.clone(),
1353                rule.clone(),
1354                scenario.clone(),
1355                world,
1356                after_hook_meta,
1357                after_hook_error,
1358                retry_num,
1359            );
1360
1361            is_failed
1362        };
1363        #[cfg(feature = "tracing")]
1364        let (is_failed, span_id) = {
1365            let span = id.scenario_span();
1366            let span_id = span.id();
1367            let is_failed = tracing::Instrument::instrument(is_failed, span);
1368            (is_failed, span_id)
1369        };
1370        let is_failed = is_failed.then_yield().await;
1371
1372        #[cfg(feature = "tracing")]
1373        if let Some((waiter, span_id)) = waiter.zip(span_id) {
1374            waiter.wait_for_span_close(span_id).then_yield().await;
1375        }
1376
1377        self.send_event(event::Cucumber::scenario(
1378            feature.clone(),
1379            rule.clone(),
1380            scenario.clone(),
1381            event::Scenario::Finished.with_retries(retry_num),
1382        ));
1383
1384        let next_try =
1385            retries.filter(|_| is_failed).and_then(RetryOptions::next_try);
1386        if let Some(next_try) = next_try {
1387            self.storage
1388                .insert_retried_scenario(
1389                    feature.clone(),
1390                    rule.clone(),
1391                    scenario,
1392                    scenario_ty,
1393                    Some(next_try),
1394                )
1395                .await;
1396        }
1397
1398        self.scenario_finished(
1399            id,
1400            feature,
1401            rule,
1402            is_failed,
1403            next_try.is_some(),
1404        );
1405    }
1406
1407    /// Executes [`HookType::Before`], if present.
1408    ///
1409    /// # Events
1410    ///
1411    /// - Emits all the [`HookType::Before`] events, except [`Hook::Failed`].
1412    ///   See [`Self::emit_failed_events()`] for more details.
1413    ///
1414    /// [`Hook::Failed`]: event::Hook::Failed
1415    async fn run_before_hook(
1416        &self,
1417        feature: &Source<gherkin::Feature>,
1418        rule: Option<&Source<gherkin::Rule>>,
1419        scenario: &Source<gherkin::Scenario>,
1420        retries: Option<Retries>,
1421        scenario_id: ScenarioId,
1422        #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1423    ) -> Result<Option<W>, ExecutionFailure<W>> {
1424        let init_world = async {
1425            AssertUnwindSafe(async { W::new().await })
1426                .catch_unwind()
1427                .then_yield()
1428                .await
1429                .map_err(Info::from)
1430                .and_then(|r| {
1431                    r.map_err(|e| {
1432                        coerce_into_info(format!(
1433                            "failed to initialize World: {e}",
1434                        ))
1435                    })
1436                })
1437                .map_err(|info| (info, None))
1438        };
1439
1440        if let Some(hook) = self.before_hook.as_ref() {
1441            self.send_event(event::Cucumber::scenario(
1442                feature.clone(),
1443                rule.cloned(),
1444                scenario.clone(),
1445                event::Scenario::hook_started(HookType::Before)
1446                    .with_retries(retries),
1447            ));
1448
1449            let fut = init_world.and_then(async |mut world| {
1450                let fut = async {
1451                    (hook)(
1452                        feature.as_ref(),
1453                        rule.as_ref().map(AsRef::as_ref),
1454                        scenario.as_ref(),
1455                        &mut world,
1456                    )
1457                    .await;
1458                };
1459                match AssertUnwindSafe(fut).catch_unwind().await {
1460                    Ok(()) => Ok(world),
1461                    Err(i) => Err((Info::from(i), Some(world))),
1462                }
1463            });
1464
1465            #[cfg(feature = "tracing")]
1466            let (fut, span_id) = {
1467                let span = scenario_id.hook_span(HookType::Before);
1468                let span_id = span.id();
1469                let fut = tracing::Instrument::instrument(fut, span);
1470                (fut, span_id)
1471            };
1472            #[cfg(not(feature = "tracing"))]
1473            let _: ScenarioId = scenario_id;
1474
1475            let result = fut.then_yield().await;
1476
1477            #[cfg(feature = "tracing")]
1478            if let Some((waiter, id)) = waiter.zip(span_id) {
1479                waiter.wait_for_span_close(id).then_yield().await;
1480            }
1481
1482            match result {
1483                Ok(world) => {
1484                    self.send_event(event::Cucumber::scenario(
1485                        feature.clone(),
1486                        rule.cloned(),
1487                        scenario.clone(),
1488                        event::Scenario::hook_passed(HookType::Before)
1489                            .with_retries(retries),
1490                    ));
1491                    Ok(Some(world))
1492                }
1493                Err((panic_info, world)) => {
1494                    Err(ExecutionFailure::BeforeHookPanicked {
1495                        world,
1496                        panic_info,
1497                        meta: event::Metadata::new(()),
1498                    })
1499                }
1500            }
1501        } else {
1502            Ok(None)
1503        }
1504    }
1505
1506    /// Runs a [`Step`].
1507    ///
1508    /// # Events
1509    ///
1510    /// - Emits all the [`Step`] events, except [`Step::Failed`]. See
1511    ///   [`Self::emit_failed_events()`] for more details.
1512    ///
1513    /// [`Step`]: gherkin::Step
1514    /// [`Step::Failed`]: event::Step::Failed
1515    async fn run_step<St, Ps, Sk>(
1516        &self,
1517        world_opt: Option<W>,
1518        step: Source<gherkin::Step>,
1519        is_background: bool,
1520        (started, passed, skipped): (St, Ps, Sk),
1521        scenario_id: ScenarioId,
1522        #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1523    ) -> Result<W, ExecutionFailure<W>>
1524    where
1525        St: FnOnce(Source<gherkin::Step>) -> event::Cucumber<W>,
1526        Ps: FnOnce(
1527            Source<gherkin::Step>,
1528            CaptureLocations,
1529            Option<step::Location>,
1530        ) -> event::Cucumber<W>,
1531        Sk: FnOnce(Source<gherkin::Step>) -> event::Cucumber<W>,
1532    {
1533        self.send_event(started(step.clone()));
1534
1535        let run = async {
1536            let (step_fn, captures, loc, ctx) =
1537                match self.collection.find(&step) {
1538                    Ok(Some(f)) => f,
1539                    Ok(None) => return Ok((None, None, world_opt)),
1540                    Err(e) => {
1541                        let e = event::StepError::AmbiguousMatch(e);
1542                        return Err((e, None, None, world_opt));
1543                    }
1544                };
1545
1546            let mut world = if let Some(w) = world_opt {
1547                w
1548            } else {
1549                match AssertUnwindSafe(async { W::new().await })
1550                    .catch_unwind()
1551                    .then_yield()
1552                    .await
1553                {
1554                    Ok(Ok(w)) => w,
1555                    Ok(Err(e)) => {
1556                        let e = event::StepError::Panic(coerce_into_info(
1557                            format!("failed to initialize `World`: {e}"),
1558                        ));
1559                        return Err((e, None, loc, None));
1560                    }
1561                    Err(e) => {
1562                        let e = event::StepError::Panic(e.into());
1563                        return Err((e, None, loc, None));
1564                    }
1565                }
1566            };
1567
1568            match AssertUnwindSafe(async { step_fn(&mut world, ctx).await })
1569                .catch_unwind()
1570                .await
1571            {
1572                Ok(()) => Ok((Some(captures), loc, Some(world))),
1573                Err(e) => {
1574                    let e = event::StepError::Panic(e.into());
1575                    Err((e, Some(captures), loc, Some(world)))
1576                }
1577            }
1578        };
1579
1580        #[cfg(feature = "tracing")]
1581        let (run, span_id) = {
1582            let span = scenario_id.step_span(is_background);
1583            let span_id = span.id();
1584            let run = tracing::Instrument::instrument(run, span);
1585            (run, span_id)
1586        };
1587        let result = run.then_yield().await;
1588
1589        #[cfg(feature = "tracing")]
1590        if let Some((waiter, id)) = waiter.zip(span_id) {
1591            waiter.wait_for_span_close(id).then_yield().await;
1592        }
1593        #[cfg(not(feature = "tracing"))]
1594        let _: ScenarioId = scenario_id;
1595
1596        match result {
1597            Ok((Some(captures), loc, Some(world))) => {
1598                self.send_event(passed(step, captures, loc));
1599                Ok(world)
1600            }
1601            Ok((_, _, world)) => {
1602                self.send_event(skipped(step));
1603                Err(ExecutionFailure::StepSkipped(world))
1604            }
1605            Err((err, captures, loc, world)) => {
1606                Err(ExecutionFailure::StepPanicked {
1607                    world,
1608                    step,
1609                    captures,
1610                    loc,
1611                    err,
1612                    meta: event::Metadata::new(()),
1613                    is_background,
1614                })
1615            }
1616        }
1617    }
1618
1619    /// Emits all the failure events of [`HookType::Before`] or [`Step`] after
1620    /// executing the [`Self::run_after_hook()`].
1621    ///
1622    /// This is done because [`HookType::After`] requires a mutable reference to
1623    /// the [`World`] while on the other hand we store immutable reference to it
1624    /// inside failure events for easier debugging. So, to avoid imposing
1625    /// additional [`Clone`] bounds on the [`World`], we run the
1626    /// [`HookType::After`] first without emitting any events about its
1627    /// execution, then emit failure event of the [`HookType::Before`] or
1628    /// [`Step`], if present, and finally emit all the [`HookType::After`]
1629    /// events. This allows us to ensure [order guarantees][1] while not
1630    /// restricting the [`HookType::After`] to the immutable reference. The only
1631    /// downside of this approach is that we may emit failure events of
1632    /// [`HookType::Before`] or [`Step`] with the [`World`] state being changed
1633    /// by the [`HookType::After`].
1634    ///
1635    /// [`Step`]: gherkin::Step
1636    /// [1]: crate::Runner#order-guarantees
1637    fn emit_failed_events(
1638        &self,
1639        feature: Source<gherkin::Feature>,
1640        rule: Option<Source<gherkin::Rule>>,
1641        scenario: Source<gherkin::Scenario>,
1642        world: Option<Arc<W>>,
1643        err: ExecutionFailure<W>,
1644        retries: Option<Retries>,
1645    ) {
1646        match err {
1647            ExecutionFailure::StepSkipped(_) => {}
1648            ExecutionFailure::BeforeHookPanicked {
1649                panic_info, meta, ..
1650            } => {
1651                self.send_event_with_meta(
1652                    event::Cucumber::scenario(
1653                        feature,
1654                        rule,
1655                        scenario,
1656                        event::Scenario::hook_failed(
1657                            HookType::Before,
1658                            world,
1659                            panic_info,
1660                        )
1661                        .with_retries(retries),
1662                    ),
1663                    meta,
1664                );
1665            }
1666            ExecutionFailure::StepPanicked {
1667                step,
1668                captures,
1669                loc,
1670                err: error,
1671                meta,
1672                is_background: true,
1673                ..
1674            } => self.send_event_with_meta(
1675                event::Cucumber::scenario(
1676                    feature,
1677                    rule,
1678                    scenario,
1679                    event::Scenario::background_step_failed(
1680                        step, captures, loc, world, error,
1681                    )
1682                    .with_retries(retries),
1683                ),
1684                meta,
1685            ),
1686            ExecutionFailure::StepPanicked {
1687                step,
1688                captures,
1689                loc,
1690                err: error,
1691                meta,
1692                is_background: false,
1693                ..
1694            } => self.send_event_with_meta(
1695                event::Cucumber::scenario(
1696                    feature,
1697                    rule,
1698                    scenario,
1699                    event::Scenario::step_failed(
1700                        step, captures, loc, world, error,
1701                    )
1702                    .with_retries(retries),
1703                ),
1704                meta,
1705            ),
1706        }
1707    }
1708
1709    /// Executes the [`HookType::After`], if present.
1710    ///
1711    /// Doesn't emit any events, see [`Self::emit_failed_events()`] for more
1712    /// details.
1713    // TODO: Needs refactoring.
1714    #[cfg_attr(
1715        feature = "tracing",
1716        expect(clippy::too_many_arguments, reason = "needs refactoring")
1717    )]
1718    async fn run_after_hook(
1719        &self,
1720        mut world: Option<W>,
1721        feature: &Source<gherkin::Feature>,
1722        rule: Option<&Source<gherkin::Rule>>,
1723        scenario: &Source<gherkin::Scenario>,
1724        ev: event::ScenarioFinished,
1725        scenario_id: ScenarioId,
1726        #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1727    ) -> Result<
1728        (Option<W>, Option<AfterHookEventsMeta>),
1729        (Option<W>, AfterHookEventsMeta, Info),
1730    > {
1731        if let Some(hook) = self.after_hook.as_ref() {
1732            let fut = async {
1733                (hook)(
1734                    feature.as_ref(),
1735                    rule.as_ref().map(AsRef::as_ref),
1736                    scenario.as_ref(),
1737                    &ev,
1738                    world.as_mut(),
1739                )
1740                .await;
1741            };
1742
1743            let started = event::Metadata::new(());
1744            let fut = AssertUnwindSafe(fut).catch_unwind();
1745
1746            #[cfg(feature = "tracing")]
1747            let (fut, span_id) = {
1748                let span = scenario_id.hook_span(HookType::After);
1749                let span_id = span.id();
1750                let fut = tracing::Instrument::instrument(fut, span);
1751                (fut, span_id)
1752            };
1753            #[cfg(not(feature = "tracing"))]
1754            let _: ScenarioId = scenario_id;
1755
1756            let res = fut.then_yield().await;
1757
1758            #[cfg(feature = "tracing")]
1759            if let Some((waiter, id)) = waiter.zip(span_id) {
1760                waiter.wait_for_span_close(id).then_yield().await;
1761            }
1762
1763            let finished = event::Metadata::new(());
1764            let meta = AfterHookEventsMeta { started, finished };
1765
1766            match res {
1767                Ok(()) => Ok((world, Some(meta))),
1768                Err(info) => Err((world, meta, info.into())),
1769            }
1770        } else {
1771            Ok((world, None))
1772        }
1773    }
1774
1775    /// Emits all the [`HookType::After`] events.
1776    ///
1777    /// See [`Self::emit_failed_events()`] for the explanation why we don't do
1778    /// that inside [`Self::run_after_hook()`].
1779    // TODO: Needs refactoring.
1780    #[expect(clippy::too_many_arguments, reason = "needs refactoring")]
1781    fn emit_after_hook_events(
1782        &self,
1783        feature: Source<gherkin::Feature>,
1784        rule: Option<Source<gherkin::Rule>>,
1785        scenario: Source<gherkin::Scenario>,
1786        world: Option<Arc<W>>,
1787        meta: Option<AfterHookEventsMeta>,
1788        err: Option<Info>,
1789        retries: Option<Retries>,
1790    ) {
1791        debug_assert_eq!(
1792            self.after_hook.is_some(),
1793            meta.is_some(),
1794            "`AfterHookEventsMeta` is not passed, despite `self.after_hook` \
1795             being set",
1796        );
1797
1798        if let Some(meta) = meta {
1799            self.send_event_with_meta(
1800                event::Cucumber::scenario(
1801                    feature.clone(),
1802                    rule.clone(),
1803                    scenario.clone(),
1804                    event::Scenario::hook_started(HookType::After)
1805                        .with_retries(retries),
1806                ),
1807                meta.started,
1808            );
1809
1810            let ev = if let Some(e) = err {
1811                event::Cucumber::scenario(
1812                    feature,
1813                    rule,
1814                    scenario,
1815                    event::Scenario::hook_failed(HookType::After, world, e)
1816                        .with_retries(retries),
1817                )
1818            } else {
1819                event::Cucumber::scenario(
1820                    feature,
1821                    rule,
1822                    scenario,
1823                    event::Scenario::hook_passed(HookType::After)
1824                        .with_retries(retries),
1825                )
1826            };
1827
1828            self.send_event_with_meta(ev, meta.finished);
1829        }
1830    }
1831
1832    /// Notifies [`FinishedRulesAndFeatures`] about [`Scenario`] being finished.
1833    ///
1834    /// [`Scenario`]: gherkin::Scenario
1835    fn scenario_finished(
1836        &self,
1837        id: ScenarioId,
1838        feature: Source<gherkin::Feature>,
1839        rule: Option<Source<gherkin::Rule>>,
1840        is_failed: IsFailed,
1841        is_retried: IsRetried,
1842    ) {
1843        // If the receiver end is dropped, then no one listens for events
1844        // so we can just ignore it.
1845        drop(
1846            self.finished_sender
1847                .unbounded_send((id, feature, rule, is_failed, is_retried)),
1848        );
1849    }
1850
1851    /// Notifies with the given [`Cucumber`] event.
1852    ///
1853    /// [`Cucumber`]: event::Cucumber
1854    fn send_event(&self, event: event::Cucumber<W>) {
1855        // If the receiver end is dropped, then no one listens for events,
1856        // so we can just ignore it.
1857        drop(self.event_sender.unbounded_send(Ok(Event::new(event))));
1858    }
1859
1860    /// Notifies with the given [`Cucumber`] event along with its [`Metadata`].
1861    ///
1862    /// [`Cucumber`]: event::Cucumber
1863    /// [`Metadata`]: event::Metadata
1864    fn send_event_with_meta(
1865        &self,
1866        event: event::Cucumber<W>,
1867        meta: event::Metadata,
1868    ) {
1869        // If the receiver end is dropped, then no one listens for events,
1870        // so we can just ignore it.
1871        drop(self.event_sender.unbounded_send(Ok(meta.wrap(event))));
1872    }
1873
1874    /// Notifies with the given [`Cucumber`] events.
1875    ///
1876    /// [`Cucumber`]: event::Cucumber
1877    fn send_all_events(
1878        &self,
1879        events: impl IntoIterator<Item = event::Cucumber<W>>,
1880    ) {
1881        for v in events {
1882            // If the receiver end is dropped, then no one listens for events,
1883            // so we can just stop from here.
1884            if self.event_sender.unbounded_send(Ok(Event::new(v))).is_err() {
1885                break;
1886            }
1887        }
1888    }
1889}
1890
1891/// ID of a [`Scenario`], uniquely identifying it.
1892///
1893/// **NOTE**: Retried [`Scenario`] has a different ID from a failed one.
1894///
1895/// [`Scenario`]: gherkin::Scenario
1896#[derive(Clone, Copy, Debug, Display, Eq, FromStr, Hash, PartialEq)]
1897pub struct ScenarioId(pub(crate) u64);
1898
1899impl ScenarioId {
1900    /// Creates a new unique [`ScenarioId`].
1901    pub fn new() -> Self {
1902        /// [`AtomicU64`] ID.
1903        static ID: AtomicU64 = AtomicU64::new(0);
1904
1905        Self(ID.fetch_add(1, Ordering::Relaxed))
1906    }
1907}
1908
1909impl Default for ScenarioId {
1910    fn default() -> Self {
1911        Self::new()
1912    }
1913}
1914
1915/// Stores currently running [`Rule`]s and [`Feature`]s and notifies about their
1916/// state of completion.
1917///
1918/// [`Feature`]: gherkin::Feature
1919/// [`Rule`]: gherkin::Rule
1920struct FinishedRulesAndFeatures {
1921    /// Number of finished [`Scenario`]s of [`Feature`].
1922    ///
1923    /// [`Feature`]: gherkin::Feature
1924    /// [`Scenario`]: gherkin::Scenario
1925    features_scenarios_count: HashMap<Source<gherkin::Feature>, usize>,
1926
1927    /// Number of finished [`Scenario`]s of [`Rule`].
1928    ///
1929    /// We also store path to a [`Feature`], so [`Rule`]s with same names and
1930    /// spans in different `.feature` files will have different hashes.
1931    ///
1932    /// [`Feature`]: gherkin::Feature
1933    /// [`Rule`]: gherkin::Rule
1934    /// [`Scenario`]: gherkin::Scenario
1935    rule_scenarios_count:
1936        HashMap<(Source<gherkin::Feature>, Source<gherkin::Rule>), usize>,
1937
1938    /// Receiver for notifying state of [`Scenario`]s completion.
1939    ///
1940    /// [`Scenario`]: gherkin::Scenario
1941    finished_receiver: FinishedFeaturesReceiver,
1942}
1943
1944/// Alias of a [`mpsc::UnboundedSender`] that notifies about finished
1945/// [`Feature`]s.
1946///
1947/// [`Feature`]: gherkin::Feature
1948type FinishedFeaturesSender = mpsc::UnboundedSender<(
1949    ScenarioId,
1950    Source<gherkin::Feature>,
1951    Option<Source<gherkin::Rule>>,
1952    IsFailed,
1953    IsRetried,
1954)>;
1955
1956/// Alias of a [`mpsc::UnboundedReceiver`] that receives events about finished
1957/// [`Feature`]s.
1958///
1959/// [`Feature`]: gherkin::Feature
1960type FinishedFeaturesReceiver = mpsc::UnboundedReceiver<(
1961    ScenarioId,
1962    Source<gherkin::Feature>,
1963    Option<Source<gherkin::Rule>>,
1964    IsFailed,
1965    IsRetried,
1966)>;
1967
1968impl FinishedRulesAndFeatures {
1969    /// Creates a new [`FinishedRulesAndFeatures`] store.
1970    fn new(finished_receiver: FinishedFeaturesReceiver) -> Self {
1971        Self {
1972            features_scenarios_count: HashMap::new(),
1973            rule_scenarios_count: HashMap::new(),
1974            finished_receiver,
1975        }
1976    }
1977
1978    /// Marks [`Rule`]'s [`Scenario`] as finished and returns [`Rule::Finished`]
1979    /// event if no [`Scenario`]s left.
1980    ///
1981    /// [`Rule`]: gherkin::Rule
1982    /// [`Rule::Finished`]: event::Rule::Finished
1983    /// [`Scenario`]: gherkin::Scenario
1984    fn rule_scenario_finished<W>(
1985        &mut self,
1986        feature: Source<gherkin::Feature>,
1987        rule: Source<gherkin::Rule>,
1988        is_retried: bool,
1989    ) -> Option<event::Cucumber<W>> {
1990        if is_retried {
1991            return None;
1992        }
1993
1994        let finished_scenarios = self
1995            .rule_scenarios_count
1996            .get_mut(&(feature.clone(), rule.clone()))
1997            .unwrap_or_else(|| panic!("no `Rule: {}`", rule.name));
1998        *finished_scenarios += 1;
1999        (rule.scenarios.len() == *finished_scenarios).then(|| {
2000            _ = self
2001                .rule_scenarios_count
2002                .remove(&(feature.clone(), rule.clone()));
2003            event::Cucumber::rule_finished(feature, rule)
2004        })
2005    }
2006
2007    /// Marks [`Feature`]'s [`Scenario`] as finished and returns
2008    /// [`Feature::Finished`] event if no [`Scenario`]s left.
2009    ///
2010    /// [`Feature`]: gherkin::Feature
2011    /// [`Feature::Finished`]: event::Feature::Finished
2012    /// [`Scenario`]: gherkin::Scenario
2013    fn feature_scenario_finished<W>(
2014        &mut self,
2015        feature: Source<gherkin::Feature>,
2016        is_retried: bool,
2017    ) -> Option<event::Cucumber<W>> {
2018        if is_retried {
2019            return None;
2020        }
2021
2022        let finished_scenarios = self
2023            .features_scenarios_count
2024            .get_mut(&feature)
2025            .unwrap_or_else(|| panic!("no `Feature: {}`", feature.name));
2026        *finished_scenarios += 1;
2027        let scenarios = feature.count_scenarios();
2028        (scenarios == *finished_scenarios).then(|| {
2029            _ = self.features_scenarios_count.remove(&feature);
2030            event::Cucumber::feature_finished(feature)
2031        })
2032    }
2033
2034    /// Marks all the unfinished [`Rule`]s and [`Feature`]s as finished, and
2035    /// returns all the appropriate finished events.
2036    ///
2037    /// [`Feature`]: gherkin::Feature
2038    /// [`Rule`]: gherkin::Rule
2039    fn finish_all_rules_and_features<W>(
2040        &mut self,
2041    ) -> impl Iterator<Item = event::Cucumber<W>> {
2042        self.rule_scenarios_count
2043            .drain()
2044            .map(|((feat, rule), _)| event::Cucumber::rule_finished(feat, rule))
2045            .chain(
2046                self.features_scenarios_count
2047                    .drain()
2048                    .map(|(feat, _)| event::Cucumber::feature_finished(feat)),
2049            )
2050    }
2051
2052    /// Marks [`Scenario`]s as started and returns [`Rule::Started`] and
2053    /// [`Feature::Started`] if given [`Scenario`] was first for particular
2054    /// [`Rule`] or [`Feature`].
2055    ///
2056    /// [`Feature`]: gherkin::Feature
2057    /// [`Feature::Started`]: event::Feature::Started
2058    /// [`Rule`]: gherkin::Rule
2059    /// [`Rule::Started`]: event::Rule::Started
2060    /// [`Scenario`]: gherkin::Scenario
2061    fn start_scenarios<W, R>(
2062        &mut self,
2063        runnable: R,
2064    ) -> impl Iterator<Item = event::Cucumber<W>> + use<W, R>
2065    where
2066        R: AsRef<
2067            [(
2068                ScenarioId,
2069                Source<gherkin::Feature>,
2070                Option<Source<gherkin::Rule>>,
2071                Source<gherkin::Scenario>,
2072                ScenarioType,
2073                Option<RetryOptions>,
2074            )],
2075        >,
2076    {
2077        let runnable = runnable.as_ref();
2078
2079        let mut started_features = Vec::new();
2080        for feature in runnable.iter().map(|(_, f, ..)| f.clone()).dedup() {
2081            _ = self
2082                .features_scenarios_count
2083                .entry(feature.clone())
2084                .or_insert_with(|| {
2085                    started_features.push(feature);
2086                    0
2087                });
2088        }
2089
2090        let mut started_rules = Vec::new();
2091        for (feat, rule) in runnable
2092            .iter()
2093            .filter_map(|(_, feat, rule, _, _, _)| {
2094                rule.clone().map(|r| (feat.clone(), r))
2095            })
2096            .dedup()
2097        {
2098            _ = self
2099                .rule_scenarios_count
2100                .entry((feat.clone(), rule.clone()))
2101                .or_insert_with(|| {
2102                    started_rules.push((feat, rule));
2103                    0
2104                });
2105        }
2106
2107        started_features
2108            .into_iter()
2109            .map(event::Cucumber::feature_started)
2110            .chain(
2111                started_rules
2112                    .into_iter()
2113                    .map(|(f, r)| event::Cucumber::rule_started(f, r)),
2114            )
2115    }
2116}
2117
2118/// [`Scenario`]s storage.
2119///
2120/// [`Scenario`]: gherkin::Scenario
2121type Scenarios = HashMap<
2122    ScenarioType,
2123    Vec<(
2124        ScenarioId,
2125        Source<gherkin::Feature>,
2126        Option<Source<gherkin::Rule>>,
2127        Source<gherkin::Scenario>,
2128        Option<RetryOptionsWithDeadline>,
2129    )>,
2130>;
2131
2132/// Alias of a [`Features::insert_scenarios()`] argument.
2133type InsertedScenarios = HashMap<
2134    ScenarioType,
2135    Vec<(
2136        ScenarioId,
2137        Source<gherkin::Feature>,
2138        Option<Source<gherkin::Rule>>,
2139        Source<gherkin::Scenario>,
2140        Option<RetryOptions>,
2141    )>,
2142>;
2143
2144/// Storage sorted by [`ScenarioType`] [`Feature`]'s [`Scenario`]s.
2145///
2146/// [`Feature`]: gherkin::Feature
2147/// [`Scenario`]: gherkin::Scenario
2148#[derive(Clone, Default)]
2149struct Features {
2150    /// Storage itself.
2151    scenarios: Arc<Mutex<Scenarios>>,
2152
2153    /// Indicates whether all parsed [`Feature`]s are sorted and stored.
2154    ///
2155    /// [`Feature`]: gherkin::Feature
2156    finished: Arc<AtomicBool>,
2157}
2158
2159impl Features {
2160    /// Splits [`Feature`] into [`Scenario`]s, sorts by [`ScenarioType`] and
2161    /// stores them.
2162    ///
2163    /// [`Feature`]: gherkin::Feature
2164    /// [`Scenario`]: gherkin::Scenario
2165    async fn insert<Which>(
2166        &self,
2167        feature: gherkin::Feature,
2168        which_scenario: &Which,
2169        retry: &RetryOptionsFn,
2170        cli: &Cli,
2171    ) where
2172        Which: Fn(
2173                &gherkin::Feature,
2174                Option<&gherkin::Rule>,
2175                &gherkin::Scenario,
2176            ) -> ScenarioType
2177            + 'static,
2178    {
2179        let feature = Source::new(feature);
2180
2181        let local = feature
2182            .scenarios
2183            .iter()
2184            .map(|s| (None, s))
2185            .chain(feature.rules.iter().flat_map(|r| {
2186                let rule = Some(Source::new(r.clone()));
2187                r.scenarios
2188                    .iter()
2189                    .map(|s| (rule.clone(), s))
2190                    .collect::<Vec<_>>()
2191            }))
2192            .map(|(rule, scenario)| {
2193                let retries = retry(&feature, rule.as_deref(), scenario, cli);
2194                (
2195                    ScenarioId::new(),
2196                    feature.clone(),
2197                    rule,
2198                    Source::new(scenario.clone()),
2199                    retries,
2200                )
2201            })
2202            .into_group_map_by(|(_, f, r, s, _)| {
2203                which_scenario(f, r.as_ref().map(AsRef::as_ref), s)
2204            });
2205
2206        self.insert_scenarios(local).await;
2207    }
2208
2209    /// Inserts the provided retried [`Scenario`] into this [`Features`]
2210    /// storage.
2211    ///
2212    /// [`Scenario`]: gherkin::Scenario
2213    async fn insert_retried_scenario(
2214        &self,
2215        feature: Source<gherkin::Feature>,
2216        rule: Option<Source<gherkin::Rule>>,
2217        scenario: Source<gherkin::Scenario>,
2218        scenario_ty: ScenarioType,
2219        retries: Option<RetryOptions>,
2220    ) {
2221        self.insert_scenarios(
2222            iter::once((
2223                scenario_ty,
2224                vec![(ScenarioId::new(), feature, rule, scenario, retries)],
2225            ))
2226            .collect(),
2227        )
2228        .await;
2229    }
2230
2231    /// Inserts the provided [`Scenario`]s into this [`Features`] storage.
2232    ///
2233    /// [`Scenario`]: gherkin::Scenario
2234    async fn insert_scenarios(&self, scenarios: InsertedScenarios) {
2235        let now = Instant::now();
2236
2237        let mut with_retries = HashMap::<_, Vec<_>>::new();
2238        let mut without_retries: Scenarios = HashMap::new();
2239        #[expect(clippy::iter_over_hash_type, reason = "order doesn't matter")]
2240        for (which, values) in scenarios {
2241            for (id, f, r, s, ret) in values {
2242                match ret {
2243                    ret @ (None
2244                    | Some(RetryOptions {
2245                        retries: Retries { current: 0, .. },
2246                        ..
2247                    })) => {
2248                        // `Retries::current` is `0`, so this `Scenario` run is
2249                        // initial, and we don't need to wait for retry delay.
2250                        let ret = ret.map(RetryOptions::without_deadline);
2251                        without_retries
2252                            .entry(which)
2253                            .or_default()
2254                            .push((id, f, r, s, ret));
2255                    }
2256                    Some(ret) => {
2257                        let ret = ret.with_deadline(now);
2258                        with_retries
2259                            .entry(which)
2260                            .or_default()
2261                            .push((id, f, r, s, ret));
2262                    }
2263                }
2264            }
2265        }
2266
2267        let mut storage = self.scenarios.lock().await;
2268
2269        #[expect(clippy::iter_over_hash_type, reason = "order doesn't matter")]
2270        for (which, values) in with_retries {
2271            let ty_storage = storage.entry(which).or_default();
2272            for (id, f, r, s, ret) in values {
2273                ty_storage.insert(0, (id, f, r, s, Some(ret)));
2274            }
2275        }
2276
2277        if without_retries.contains_key(&ScenarioType::Serial) {
2278            // If there are Serial Scenarios we insert all Serial and Concurrent
2279            // Scenarios in front.
2280            // This is done to execute them closely to one another, so the
2281            // output wouldn't hang on executing other Concurrent Scenarios.
2282            #[expect(
2283                clippy::iter_over_hash_type,
2284                reason = "order doesn't matter"
2285            )]
2286            for (which, mut values) in without_retries {
2287                let old = mem::take(storage.entry(which).or_default());
2288                values.extend(old);
2289                storage.entry(which).or_default().extend(values);
2290            }
2291        } else {
2292            // If there are no Serial Scenarios, we just extend already existing
2293            // Concurrent Scenarios.
2294            #[expect(
2295                clippy::iter_over_hash_type,
2296                reason = "order doesn't matter"
2297            )]
2298            for (which, values) in without_retries {
2299                storage.entry(which).or_default().extend(values);
2300            }
2301        }
2302    }
2303
2304    /// Returns [`Scenario`]s which are ready to run and the minimal deadline of
2305    /// all retried [`Scenario`]s.
2306    ///
2307    /// [`Scenario`]: gherkin::Scenario
2308    #[expect(clippy::significant_drop_tightening, reason = "false positive")]
2309    async fn get(
2310        &self,
2311        max_concurrent_scenarios: Option<usize>,
2312    ) -> (
2313        Vec<(
2314            ScenarioId,
2315            Source<gherkin::Feature>,
2316            Option<Source<gherkin::Rule>>,
2317            Source<gherkin::Scenario>,
2318            ScenarioType,
2319            Option<RetryOptions>,
2320        )>,
2321        Option<Duration>,
2322    ) {
2323        use RetryOptionsWithDeadline as WithDeadline;
2324        use ScenarioType::{Concurrent, Serial};
2325
2326        if max_concurrent_scenarios == Some(0) {
2327            return (Vec::new(), None);
2328        }
2329
2330        let mut min_dur = None;
2331        let mut drain =
2332            |storage: &mut Vec<(_, _, _, _, Option<WithDeadline>)>,
2333             ty,
2334             count: Option<usize>| {
2335                let mut i = 0;
2336                let drained = storage
2337                    .extract_if(.., |(_, _, _, _, ret)| {
2338                        // Because of retries involved, we cannot just specify
2339                        // `..count` range to `.extract_if()`.
2340                        if count.as_ref().is_some_and(|c| i >= *c) {
2341                            return false;
2342                        }
2343
2344                        ret.as_ref()
2345                            .and_then(WithDeadline::left_until_retry)
2346                            .map_or_else(
2347                                || {
2348                                    i += 1;
2349                                    true
2350                                },
2351                                |left| {
2352                                    min_dur = min_dur
2353                                        .map(|min| cmp::min(min, left))
2354                                        .or(Some(left));
2355                                    false
2356                                },
2357                            )
2358                    })
2359                    .map(|(id, f, r, s, ret)| {
2360                        (id, f, r, s, ty, ret.map(Into::into))
2361                    })
2362                    .collect::<Vec<_>>();
2363                (!drained.is_empty()).then_some(drained)
2364            };
2365
2366        let mut guard = self.scenarios.lock().await;
2367        let scenarios = guard
2368            .get_mut(&Serial)
2369            .and_then(|storage| drain(storage, Serial, Some(1)))
2370            .or_else(|| {
2371                guard.get_mut(&Concurrent).and_then(|storage| {
2372                    drain(storage, Concurrent, max_concurrent_scenarios)
2373                })
2374            })
2375            .unwrap_or_default();
2376
2377        (scenarios, min_dur)
2378    }
2379
2380    /// Marks that there will be no more [`Feature`]s to execute.
2381    ///
2382    /// [`Feature`]: gherkin::Feature
2383    fn finish(&self) {
2384        self.finished.store(true, Ordering::SeqCst);
2385    }
2386
2387    /// Indicates whether there are more [`Feature`]s to execute.
2388    ///
2389    /// `fail_fast` argument indicates whether not yet executed scenarios should
2390    /// be omitted.
2391    ///
2392    /// [`Feature`]: gherkin::Feature
2393    async fn is_finished(&self, fail_fast: bool) -> bool {
2394        self.finished.load(Ordering::SeqCst)
2395            && (fail_fast
2396                || self.scenarios.lock().await.values().all(Vec::is_empty))
2397    }
2398}
2399
2400/// Coerces the given `value` into a type-erased [`Info`].
2401fn coerce_into_info<T: Any + Send + 'static>(val: T) -> Info {
2402    Arc::new(val)
2403}
2404
2405/// Failure encountered during execution of [`HookType::Before`] or [`Step`].
2406/// See [`Executor::emit_failed_events()`] for more info.
2407///
2408/// [`Step`]: gherkin::Step
2409enum ExecutionFailure<World> {
2410    /// [`HookType::Before`] panicked.
2411    BeforeHookPanicked {
2412        /// [`World`] at the time [`HookType::Before`] has panicked.
2413        world: Option<World>,
2414
2415        /// [`catch_unwind()`] of the [`HookType::Before`] panic.
2416        ///
2417        /// [`catch_unwind()`]: std::panic::catch_unwind
2418        panic_info: Info,
2419
2420        /// [`Metadata`] at the time [`HookType::Before`] panicked.
2421        ///
2422        /// [`Metadata`]: event::Metadata
2423        meta: event::Metadata,
2424    },
2425
2426    /// [`Step`] was skipped.
2427    ///
2428    /// [`Step`]: gherkin::Step.
2429    StepSkipped(Option<World>),
2430
2431    /// [`Step`] failed.
2432    ///
2433    /// [`Step`]: gherkin::Step.
2434    StepPanicked {
2435        /// [`World`] at the time when [`Step`] has failed.
2436        ///
2437        /// [`Step`]: gherkin::Step
2438        world: Option<World>,
2439
2440        /// [`Step`] itself.
2441        ///
2442        /// [`Step`]: gherkin::Step
2443        step: Source<gherkin::Step>,
2444
2445        /// [`Step`]s [`regex`] [`CaptureLocations`].
2446        ///
2447        /// [`Step`]: gherkin::Step
2448        captures: Option<CaptureLocations>,
2449
2450        /// [`Location`] of the [`fn`] that matched this [`Step`].
2451        ///
2452        /// [`Location`]: step::Location
2453        /// [`Step`]: gherkin::Step
2454        loc: Option<step::Location>,
2455
2456        /// [`StepError`] of the [`Step`].
2457        ///
2458        /// [`Step`]: gherkin::Step
2459        /// [`StepError`]: event::StepError
2460        err: event::StepError,
2461
2462        /// [`Metadata`] at the time when [`Step`] failed.
2463        ///
2464        /// [`Metadata`]: event::Metadata
2465        /// [`Step`]: gherkin::Step.
2466        meta: event::Metadata,
2467
2468        /// Indicator whether the [`Step`] was background or not.
2469        ///
2470        /// [`Step`]: gherkin::Step
2471        is_background: bool,
2472    },
2473}
2474
2475/// [`Metadata`] of [`HookType::After`] events.
2476///
2477/// [`Metadata`]: event::Metadata
2478struct AfterHookEventsMeta {
2479    /// [`Metadata`] at the time [`HookType::After`] started.
2480    ///
2481    /// [`Metadata`]: event::Metadata
2482    started: event::Metadata,
2483
2484    /// [`Metadata`] at the time [`HookType::After`] finished.
2485    ///
2486    /// [`Metadata`]: event::Metadata
2487    finished: event::Metadata,
2488}
2489
2490impl<W> ExecutionFailure<W> {
2491    /// Takes the [`World`] leaving a [`None`] in its place.
2492    const fn take_world(&mut self) -> Option<W> {
2493        match self {
2494            Self::BeforeHookPanicked { world, .. }
2495            | Self::StepSkipped(world)
2496            | Self::StepPanicked { world, .. } => world.take(),
2497        }
2498    }
2499
2500    /// Creates an [`event::ScenarioFinished`] from this [`ExecutionFailure`].
2501    fn get_scenario_finished_event(&self) -> event::ScenarioFinished {
2502        use event::ScenarioFinished::{
2503            BeforeHookFailed, StepFailed, StepSkipped,
2504        };
2505
2506        match self {
2507            Self::BeforeHookPanicked { panic_info, .. } => {
2508                BeforeHookFailed(Arc::clone(panic_info))
2509            }
2510            Self::StepSkipped(_) => StepSkipped,
2511            Self::StepPanicked { captures, loc, err, .. } => {
2512                StepFailed(captures.clone(), *loc, err.clone())
2513            }
2514        }
2515    }
2516}
2517
2518#[cfg(test)]
2519mod retry_options {
2520    use gherkin::GherkinEnv;
2521    use humantime::parse_duration;
2522
2523    use super::{Cli, Duration, Retries, RetryOptions};
2524
2525    mod scenario_tags {
2526        use super::*;
2527
2528        // language=Gherkin
2529        const FEATURE: &str = r"
2530Feature: only scenarios
2531  Scenario: no tags
2532    Given a step
2533
2534  @retry
2535  Scenario: tag
2536    Given a step
2537
2538  @retry(5)
2539  Scenario: tag with explicit value
2540    Given a step
2541
2542  @retry.after(3s)
2543  Scenario: tag with explicit after
2544    Given a step
2545
2546  @retry(5).after(15s)
2547  Scenario: tag with explicit value and after
2548    Given a step
2549";
2550
2551        #[test]
2552        fn empty_cli() {
2553            let cli = Cli {
2554                concurrency: None,
2555                fail_fast: false,
2556                retry: None,
2557                retry_after: None,
2558                retry_tag_filter: None,
2559            };
2560            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2561                .expect("failed to parse feature");
2562
2563            assert_eq!(
2564                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2565                None,
2566            );
2567            assert_eq!(
2568                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2569                Some(RetryOptions {
2570                    retries: Retries { current: 0, left: 1 },
2571                    after: None,
2572                }),
2573            );
2574            assert_eq!(
2575                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2576                Some(RetryOptions {
2577                    retries: Retries { current: 0, left: 5 },
2578                    after: None,
2579                }),
2580            );
2581            assert_eq!(
2582                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2583                Some(RetryOptions {
2584                    retries: Retries { current: 0, left: 1 },
2585                    after: Some(Duration::from_secs(3)),
2586                }),
2587            );
2588            assert_eq!(
2589                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2590                Some(RetryOptions {
2591                    retries: Retries { current: 0, left: 5 },
2592                    after: Some(Duration::from_secs(15)),
2593                }),
2594            );
2595        }
2596
2597        #[test]
2598        fn cli_retries() {
2599            let cli = Cli {
2600                concurrency: None,
2601                fail_fast: false,
2602                retry: Some(7),
2603                retry_after: None,
2604                retry_tag_filter: None,
2605            };
2606            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2607                .expect("failed to parse feature");
2608
2609            assert_eq!(
2610                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2611                Some(RetryOptions {
2612                    retries: Retries { current: 0, left: 7 },
2613                    after: None,
2614                }),
2615            );
2616            assert_eq!(
2617                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2618                Some(RetryOptions {
2619                    retries: Retries { current: 0, left: 7 },
2620                    after: None,
2621                }),
2622            );
2623            assert_eq!(
2624                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2625                Some(RetryOptions {
2626                    retries: Retries { current: 0, left: 5 },
2627                    after: None,
2628                }),
2629            );
2630            assert_eq!(
2631                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2632                Some(RetryOptions {
2633                    retries: Retries { current: 0, left: 7 },
2634                    after: Some(Duration::from_secs(3)),
2635                }),
2636            );
2637            assert_eq!(
2638                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2639                Some(RetryOptions {
2640                    retries: Retries { current: 0, left: 5 },
2641                    after: Some(Duration::from_secs(15)),
2642                }),
2643            );
2644        }
2645
2646        #[test]
2647        fn cli_retry_after() {
2648            let cli = Cli {
2649                concurrency: None,
2650                fail_fast: false,
2651                retry: Some(7),
2652                retry_after: Some(parse_duration("5s").unwrap()),
2653                retry_tag_filter: None,
2654            };
2655            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2656                .expect("failed to parse feature");
2657
2658            assert_eq!(
2659                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2660                Some(RetryOptions {
2661                    retries: Retries { current: 0, left: 7 },
2662                    after: Some(Duration::from_secs(5)),
2663                }),
2664            );
2665            assert_eq!(
2666                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2667                Some(RetryOptions {
2668                    retries: Retries { current: 0, left: 7 },
2669                    after: Some(Duration::from_secs(5)),
2670                }),
2671            );
2672            assert_eq!(
2673                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2674                Some(RetryOptions {
2675                    retries: Retries { current: 0, left: 5 },
2676                    after: Some(Duration::from_secs(5)),
2677                }),
2678            );
2679            assert_eq!(
2680                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2681                Some(RetryOptions {
2682                    retries: Retries { current: 0, left: 7 },
2683                    after: Some(Duration::from_secs(3)),
2684                }),
2685            );
2686            assert_eq!(
2687                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2688                Some(RetryOptions {
2689                    retries: Retries { current: 0, left: 5 },
2690                    after: Some(Duration::from_secs(15)),
2691                }),
2692            );
2693        }
2694
2695        #[test]
2696        fn cli_retry_filter() {
2697            let cli = Cli {
2698                concurrency: None,
2699                fail_fast: false,
2700                retry: Some(7),
2701                retry_after: None,
2702                retry_tag_filter: Some("@retry".parse().unwrap()),
2703            };
2704            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2705                .expect("failed to parse feature");
2706
2707            assert_eq!(
2708                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2709                None,
2710            );
2711            assert_eq!(
2712                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2713                Some(RetryOptions {
2714                    retries: Retries { current: 0, left: 7 },
2715                    after: None,
2716                }),
2717            );
2718            assert_eq!(
2719                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2720                Some(RetryOptions {
2721                    retries: Retries { current: 0, left: 5 },
2722                    after: None,
2723                }),
2724            );
2725            assert_eq!(
2726                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2727                Some(RetryOptions {
2728                    retries: Retries { current: 0, left: 7 },
2729                    after: Some(Duration::from_secs(3)),
2730                }),
2731            );
2732            assert_eq!(
2733                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2734                Some(RetryOptions {
2735                    retries: Retries { current: 0, left: 5 },
2736                    after: Some(Duration::from_secs(15)),
2737                }),
2738            );
2739        }
2740
2741        #[test]
2742        fn cli_retry_after_and_filter() {
2743            let cli = Cli {
2744                concurrency: None,
2745                fail_fast: false,
2746                retry: Some(7),
2747                retry_after: Some(parse_duration("5s").unwrap()),
2748                retry_tag_filter: Some("@retry".parse().unwrap()),
2749            };
2750            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2751                .expect("failed to parse feature");
2752
2753            assert_eq!(
2754                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2755                None,
2756            );
2757            assert_eq!(
2758                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2759                Some(RetryOptions {
2760                    retries: Retries { current: 0, left: 7 },
2761                    after: Some(Duration::from_secs(5)),
2762                }),
2763            );
2764            assert_eq!(
2765                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2766                Some(RetryOptions {
2767                    retries: Retries { current: 0, left: 5 },
2768                    after: Some(Duration::from_secs(5)),
2769                }),
2770            );
2771            assert_eq!(
2772                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2773                Some(RetryOptions {
2774                    retries: Retries { current: 0, left: 7 },
2775                    after: Some(Duration::from_secs(3)),
2776                }),
2777            );
2778            assert_eq!(
2779                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2780                Some(RetryOptions {
2781                    retries: Retries { current: 0, left: 5 },
2782                    after: Some(Duration::from_secs(15)),
2783                }),
2784            );
2785        }
2786    }
2787
2788    mod rule_tags {
2789        use super::*;
2790
2791        // language=Gherkin
2792        const FEATURE: &str = r#"
2793Feature: only scenarios
2794  Rule: no tags
2795    Scenario: no tags
2796      Given a step
2797
2798    @retry
2799    Scenario: tag
2800      Given a step
2801
2802    @retry(5)
2803    Scenario: tag with explicit value
2804      Given a step
2805
2806    @retry.after(3s)
2807    Scenario: tag with explicit after
2808      Given a step
2809
2810    @retry(5).after(15s)
2811    Scenario: tag with explicit value and after
2812      Given a step
2813
2814  @retry(3).after(5s)
2815  Rule: retry tag
2816    Scenario: no tags
2817      Given a step
2818
2819    @retry
2820    Scenario: tag
2821      Given a step
2822
2823    @retry(5)
2824    Scenario: tag with explicit value
2825      Given a step
2826
2827    @retry.after(3s)
2828    Scenario: tag with explicit after
2829      Given a step
2830
2831    @retry(5).after(15s)
2832    Scenario: tag with explicit value and after
2833      Given a step
2834"#;
2835
2836        #[test]
2837        fn empty_cli() {
2838            let cli = Cli {
2839                concurrency: None,
2840                fail_fast: false,
2841                retry: None,
2842                retry_after: None,
2843                retry_tag_filter: None,
2844            };
2845            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2846                .expect("failed to parse feature");
2847
2848            assert_eq!(
2849                RetryOptions::parse_from_tags(
2850                    &f,
2851                    Some(&f.rules[0]),
2852                    &f.rules[0].scenarios[0],
2853                    &cli
2854                ),
2855                None,
2856            );
2857            assert_eq!(
2858                RetryOptions::parse_from_tags(
2859                    &f,
2860                    Some(&f.rules[0]),
2861                    &f.rules[0].scenarios[1],
2862                    &cli
2863                ),
2864                Some(RetryOptions {
2865                    retries: Retries { current: 0, left: 1 },
2866                    after: None,
2867                }),
2868            );
2869            assert_eq!(
2870                RetryOptions::parse_from_tags(
2871                    &f,
2872                    Some(&f.rules[0]),
2873                    &f.rules[0].scenarios[2],
2874                    &cli
2875                ),
2876                Some(RetryOptions {
2877                    retries: Retries { current: 0, left: 5 },
2878                    after: None,
2879                }),
2880            );
2881            assert_eq!(
2882                RetryOptions::parse_from_tags(
2883                    &f,
2884                    Some(&f.rules[0]),
2885                    &f.rules[0].scenarios[3],
2886                    &cli
2887                ),
2888                Some(RetryOptions {
2889                    retries: Retries { current: 0, left: 1 },
2890                    after: Some(Duration::from_secs(3)),
2891                }),
2892            );
2893            assert_eq!(
2894                RetryOptions::parse_from_tags(
2895                    &f,
2896                    Some(&f.rules[0]),
2897                    &f.rules[0].scenarios[4],
2898                    &cli
2899                ),
2900                Some(RetryOptions {
2901                    retries: Retries { current: 0, left: 5 },
2902                    after: Some(Duration::from_secs(15)),
2903                }),
2904            );
2905            assert_eq!(
2906                RetryOptions::parse_from_tags(
2907                    &f,
2908                    Some(&f.rules[1]),
2909                    &f.rules[1].scenarios[0],
2910                    &cli
2911                ),
2912                Some(RetryOptions {
2913                    retries: Retries { current: 0, left: 3 },
2914                    after: Some(Duration::from_secs(5)),
2915                }),
2916            );
2917            assert_eq!(
2918                RetryOptions::parse_from_tags(
2919                    &f,
2920                    Some(&f.rules[1]),
2921                    &f.rules[1].scenarios[1],
2922                    &cli
2923                ),
2924                Some(RetryOptions {
2925                    retries: Retries { current: 0, left: 1 },
2926                    after: None,
2927                }),
2928            );
2929            assert_eq!(
2930                RetryOptions::parse_from_tags(
2931                    &f,
2932                    Some(&f.rules[1]),
2933                    &f.rules[1].scenarios[2],
2934                    &cli
2935                ),
2936                Some(RetryOptions {
2937                    retries: Retries { current: 0, left: 5 },
2938                    after: None,
2939                }),
2940            );
2941            assert_eq!(
2942                RetryOptions::parse_from_tags(
2943                    &f,
2944                    Some(&f.rules[1]),
2945                    &f.rules[1].scenarios[3],
2946                    &cli
2947                ),
2948                Some(RetryOptions {
2949                    retries: Retries { current: 0, left: 1 },
2950                    after: Some(Duration::from_secs(3)),
2951                }),
2952            );
2953            assert_eq!(
2954                RetryOptions::parse_from_tags(
2955                    &f,
2956                    Some(&f.rules[1]),
2957                    &f.rules[1].scenarios[4],
2958                    &cli
2959                ),
2960                Some(RetryOptions {
2961                    retries: Retries { current: 0, left: 5 },
2962                    after: Some(Duration::from_secs(15)),
2963                }),
2964            );
2965        }
2966
2967        #[test]
2968        fn cli_retry_after_and_filter() {
2969            let cli = Cli {
2970                concurrency: None,
2971                fail_fast: false,
2972                retry: Some(7),
2973                retry_after: Some(parse_duration("5s").unwrap()),
2974                retry_tag_filter: Some("@retry".parse().unwrap()),
2975            };
2976            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2977                .expect("failed to parse feature");
2978
2979            assert_eq!(
2980                RetryOptions::parse_from_tags(
2981                    &f,
2982                    Some(&f.rules[0]),
2983                    &f.rules[0].scenarios[0],
2984                    &cli
2985                ),
2986                None,
2987            );
2988            assert_eq!(
2989                RetryOptions::parse_from_tags(
2990                    &f,
2991                    Some(&f.rules[0]),
2992                    &f.rules[0].scenarios[1],
2993                    &cli
2994                ),
2995                Some(RetryOptions {
2996                    retries: Retries { current: 0, left: 7 },
2997                    after: Some(Duration::from_secs(5)),
2998                }),
2999            );
3000            assert_eq!(
3001                RetryOptions::parse_from_tags(
3002                    &f,
3003                    Some(&f.rules[0]),
3004                    &f.rules[0].scenarios[2],
3005                    &cli
3006                ),
3007                Some(RetryOptions {
3008                    retries: Retries { current: 0, left: 5 },
3009                    after: Some(Duration::from_secs(5)),
3010                }),
3011            );
3012            assert_eq!(
3013                RetryOptions::parse_from_tags(
3014                    &f,
3015                    Some(&f.rules[0]),
3016                    &f.rules[0].scenarios[3],
3017                    &cli
3018                ),
3019                Some(RetryOptions {
3020                    retries: Retries { current: 0, left: 7 },
3021                    after: Some(Duration::from_secs(3)),
3022                }),
3023            );
3024            assert_eq!(
3025                RetryOptions::parse_from_tags(
3026                    &f,
3027                    Some(&f.rules[0]),
3028                    &f.rules[0].scenarios[4],
3029                    &cli
3030                ),
3031                Some(RetryOptions {
3032                    retries: Retries { current: 0, left: 5 },
3033                    after: Some(Duration::from_secs(15)),
3034                }),
3035            );
3036            assert_eq!(
3037                RetryOptions::parse_from_tags(
3038                    &f,
3039                    Some(&f.rules[1]),
3040                    &f.rules[1].scenarios[0],
3041                    &cli
3042                ),
3043                Some(RetryOptions {
3044                    retries: Retries { current: 0, left: 3 },
3045                    after: Some(Duration::from_secs(5)),
3046                }),
3047            );
3048            assert_eq!(
3049                RetryOptions::parse_from_tags(
3050                    &f,
3051                    Some(&f.rules[1]),
3052                    &f.rules[1].scenarios[1],
3053                    &cli
3054                ),
3055                Some(RetryOptions {
3056                    retries: Retries { current: 0, left: 7 },
3057                    after: Some(Duration::from_secs(5)),
3058                }),
3059            );
3060            assert_eq!(
3061                RetryOptions::parse_from_tags(
3062                    &f,
3063                    Some(&f.rules[1]),
3064                    &f.rules[1].scenarios[2],
3065                    &cli
3066                ),
3067                Some(RetryOptions {
3068                    retries: Retries { current: 0, left: 5 },
3069                    after: Some(Duration::from_secs(5)),
3070                }),
3071            );
3072            assert_eq!(
3073                RetryOptions::parse_from_tags(
3074                    &f,
3075                    Some(&f.rules[1]),
3076                    &f.rules[1].scenarios[3],
3077                    &cli
3078                ),
3079                Some(RetryOptions {
3080                    retries: Retries { current: 0, left: 7 },
3081                    after: Some(Duration::from_secs(3)),
3082                }),
3083            );
3084            assert_eq!(
3085                RetryOptions::parse_from_tags(
3086                    &f,
3087                    Some(&f.rules[1]),
3088                    &f.rules[1].scenarios[4],
3089                    &cli
3090                ),
3091                Some(RetryOptions {
3092                    retries: Retries { current: 0, left: 5 },
3093                    after: Some(Duration::from_secs(15)),
3094                }),
3095            );
3096        }
3097    }
3098
3099    mod feature_tags {
3100        use super::*;
3101
3102        // language=Gherkin
3103        const FEATURE: &str = r"
3104@retry(8)
3105Feature: only scenarios
3106  Scenario: no tags
3107    Given a step
3108
3109  @retry
3110  Scenario: tag
3111    Given a step
3112
3113  @retry(5)
3114  Scenario: tag with explicit value
3115    Given a step
3116
3117  @retry.after(3s)
3118  Scenario: tag with explicit after
3119    Given a step
3120
3121  @retry(5).after(15s)
3122  Scenario: tag with explicit value and after
3123    Given a step
3124
3125  Rule: no tags
3126    Scenario: no tags
3127      Given a step
3128
3129    @retry
3130    Scenario: tag
3131      Given a step
3132
3133    @retry(5)
3134    Scenario: tag with explicit value
3135      Given a step
3136
3137    @retry.after(3s)
3138    Scenario: tag with explicit after
3139      Given a step
3140
3141    @retry(5).after(15s)
3142    Scenario: tag with explicit value and after
3143      Given a step
3144
3145  @retry(3).after(5s)
3146  Rule: retry tag
3147    Scenario: no tags
3148      Given a step
3149
3150    @retry
3151    Scenario: tag
3152      Given a step
3153
3154    @retry(5)
3155    Scenario: tag with explicit value
3156      Given a step
3157
3158    @retry.after(3s)
3159    Scenario: tag with explicit after
3160      Given a step
3161
3162    @retry(5).after(15s)
3163    Scenario: tag with explicit value and after
3164      Given a step
3165";
3166
3167        #[test]
3168        fn empty_cli() {
3169            let cli = Cli {
3170                concurrency: None,
3171                fail_fast: false,
3172                retry: None,
3173                retry_after: None,
3174                retry_tag_filter: None,
3175            };
3176            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3177                .unwrap_or_else(|e| panic!("failed to parse feature: {e}"));
3178
3179            assert_eq!(
3180                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3181                Some(RetryOptions {
3182                    retries: Retries { current: 0, left: 8 },
3183                    after: None,
3184                }),
3185            );
3186            assert_eq!(
3187                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3188                Some(RetryOptions {
3189                    retries: Retries { current: 0, left: 1 },
3190                    after: None,
3191                }),
3192            );
3193            assert_eq!(
3194                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3195                Some(RetryOptions {
3196                    retries: Retries { current: 0, left: 5 },
3197                    after: None,
3198                }),
3199            );
3200            assert_eq!(
3201                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3202                Some(RetryOptions {
3203                    retries: Retries { current: 0, left: 1 },
3204                    after: Some(Duration::from_secs(3)),
3205                }),
3206            );
3207            assert_eq!(
3208                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3209                Some(RetryOptions {
3210                    retries: Retries { current: 0, left: 5 },
3211                    after: Some(Duration::from_secs(15)),
3212                }),
3213            );
3214            assert_eq!(
3215                RetryOptions::parse_from_tags(
3216                    &f,
3217                    Some(&f.rules[0]),
3218                    &f.rules[0].scenarios[0],
3219                    &cli
3220                ),
3221                Some(RetryOptions {
3222                    retries: Retries { current: 0, left: 8 },
3223                    after: None,
3224                }),
3225            );
3226            assert_eq!(
3227                RetryOptions::parse_from_tags(
3228                    &f,
3229                    Some(&f.rules[0]),
3230                    &f.rules[0].scenarios[1],
3231                    &cli
3232                ),
3233                Some(RetryOptions {
3234                    retries: Retries { current: 0, left: 1 },
3235                    after: None,
3236                }),
3237            );
3238            assert_eq!(
3239                RetryOptions::parse_from_tags(
3240                    &f,
3241                    Some(&f.rules[0]),
3242                    &f.rules[0].scenarios[2],
3243                    &cli
3244                ),
3245                Some(RetryOptions {
3246                    retries: Retries { current: 0, left: 5 },
3247                    after: None,
3248                }),
3249            );
3250            assert_eq!(
3251                RetryOptions::parse_from_tags(
3252                    &f,
3253                    Some(&f.rules[0]),
3254                    &f.rules[0].scenarios[3],
3255                    &cli
3256                ),
3257                Some(RetryOptions {
3258                    retries: Retries { current: 0, left: 1 },
3259                    after: Some(Duration::from_secs(3)),
3260                }),
3261            );
3262            assert_eq!(
3263                RetryOptions::parse_from_tags(
3264                    &f,
3265                    Some(&f.rules[0]),
3266                    &f.rules[0].scenarios[4],
3267                    &cli
3268                ),
3269                Some(RetryOptions {
3270                    retries: Retries { current: 0, left: 5 },
3271                    after: Some(Duration::from_secs(15)),
3272                }),
3273            );
3274            assert_eq!(
3275                RetryOptions::parse_from_tags(
3276                    &f,
3277                    Some(&f.rules[1]),
3278                    &f.rules[1].scenarios[0],
3279                    &cli
3280                ),
3281                Some(RetryOptions {
3282                    retries: Retries { current: 0, left: 3 },
3283                    after: Some(Duration::from_secs(5)),
3284                }),
3285            );
3286            assert_eq!(
3287                RetryOptions::parse_from_tags(
3288                    &f,
3289                    Some(&f.rules[1]),
3290                    &f.rules[1].scenarios[1],
3291                    &cli
3292                ),
3293                Some(RetryOptions {
3294                    retries: Retries { current: 0, left: 1 },
3295                    after: None,
3296                }),
3297            );
3298            assert_eq!(
3299                RetryOptions::parse_from_tags(
3300                    &f,
3301                    Some(&f.rules[1]),
3302                    &f.rules[1].scenarios[2],
3303                    &cli
3304                ),
3305                Some(RetryOptions {
3306                    retries: Retries { current: 0, left: 5 },
3307                    after: None,
3308                }),
3309            );
3310            assert_eq!(
3311                RetryOptions::parse_from_tags(
3312                    &f,
3313                    Some(&f.rules[1]),
3314                    &f.rules[1].scenarios[3],
3315                    &cli
3316                ),
3317                Some(RetryOptions {
3318                    retries: Retries { current: 0, left: 1 },
3319                    after: Some(Duration::from_secs(3)),
3320                }),
3321            );
3322            assert_eq!(
3323                RetryOptions::parse_from_tags(
3324                    &f,
3325                    Some(&f.rules[1]),
3326                    &f.rules[1].scenarios[4],
3327                    &cli
3328                ),
3329                Some(RetryOptions {
3330                    retries: Retries { current: 0, left: 5 },
3331                    after: Some(Duration::from_secs(15)),
3332                }),
3333            );
3334        }
3335
3336        #[test]
3337        fn cli_retry_after_and_filter() {
3338            let cli = Cli {
3339                concurrency: None,
3340                fail_fast: false,
3341                retry: Some(7),
3342                retry_after: Some(parse_duration("5s").unwrap()),
3343                retry_tag_filter: Some("@retry".parse().unwrap()),
3344            };
3345            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3346                .expect("failed to parse feature");
3347
3348            assert_eq!(
3349                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3350                Some(RetryOptions {
3351                    retries: Retries { current: 0, left: 8 },
3352                    after: Some(Duration::from_secs(5)),
3353                }),
3354            );
3355            assert_eq!(
3356                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3357                Some(RetryOptions {
3358                    retries: Retries { current: 0, left: 7 },
3359                    after: Some(Duration::from_secs(5)),
3360                }),
3361            );
3362            assert_eq!(
3363                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3364                Some(RetryOptions {
3365                    retries: Retries { current: 0, left: 5 },
3366                    after: Some(Duration::from_secs(5)),
3367                }),
3368            );
3369            assert_eq!(
3370                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3371                Some(RetryOptions {
3372                    retries: Retries { current: 0, left: 7 },
3373                    after: Some(Duration::from_secs(3)),
3374                }),
3375            );
3376            assert_eq!(
3377                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3378                Some(RetryOptions {
3379                    retries: Retries { current: 0, left: 5 },
3380                    after: Some(Duration::from_secs(15)),
3381                }),
3382            );
3383            assert_eq!(
3384                RetryOptions::parse_from_tags(
3385                    &f,
3386                    Some(&f.rules[0]),
3387                    &f.rules[0].scenarios[0],
3388                    &cli
3389                ),
3390                Some(RetryOptions {
3391                    retries: Retries { current: 0, left: 8 },
3392                    after: Some(Duration::from_secs(5)),
3393                }),
3394            );
3395            assert_eq!(
3396                RetryOptions::parse_from_tags(
3397                    &f,
3398                    Some(&f.rules[0]),
3399                    &f.rules[0].scenarios[1],
3400                    &cli
3401                ),
3402                Some(RetryOptions {
3403                    retries: Retries { current: 0, left: 7 },
3404                    after: Some(Duration::from_secs(5)),
3405                }),
3406            );
3407            assert_eq!(
3408                RetryOptions::parse_from_tags(
3409                    &f,
3410                    Some(&f.rules[0]),
3411                    &f.rules[0].scenarios[2],
3412                    &cli
3413                ),
3414                Some(RetryOptions {
3415                    retries: Retries { current: 0, left: 5 },
3416                    after: Some(Duration::from_secs(5)),
3417                }),
3418            );
3419            assert_eq!(
3420                RetryOptions::parse_from_tags(
3421                    &f,
3422                    Some(&f.rules[0]),
3423                    &f.rules[0].scenarios[3],
3424                    &cli
3425                ),
3426                Some(RetryOptions {
3427                    retries: Retries { current: 0, left: 7 },
3428                    after: Some(Duration::from_secs(3)),
3429                }),
3430            );
3431            assert_eq!(
3432                RetryOptions::parse_from_tags(
3433                    &f,
3434                    Some(&f.rules[0]),
3435                    &f.rules[0].scenarios[4],
3436                    &cli
3437                ),
3438                Some(RetryOptions {
3439                    retries: Retries { current: 0, left: 5 },
3440                    after: Some(Duration::from_secs(15)),
3441                }),
3442            );
3443            assert_eq!(
3444                RetryOptions::parse_from_tags(
3445                    &f,
3446                    Some(&f.rules[1]),
3447                    &f.rules[1].scenarios[0],
3448                    &cli
3449                ),
3450                Some(RetryOptions {
3451                    retries: Retries { current: 0, left: 3 },
3452                    after: Some(Duration::from_secs(5)),
3453                }),
3454            );
3455            assert_eq!(
3456                RetryOptions::parse_from_tags(
3457                    &f,
3458                    Some(&f.rules[1]),
3459                    &f.rules[1].scenarios[1],
3460                    &cli
3461                ),
3462                Some(RetryOptions {
3463                    retries: Retries { current: 0, left: 7 },
3464                    after: Some(Duration::from_secs(5)),
3465                }),
3466            );
3467            assert_eq!(
3468                RetryOptions::parse_from_tags(
3469                    &f,
3470                    Some(&f.rules[1]),
3471                    &f.rules[1].scenarios[2],
3472                    &cli
3473                ),
3474                Some(RetryOptions {
3475                    retries: Retries { current: 0, left: 5 },
3476                    after: Some(Duration::from_secs(5)),
3477                }),
3478            );
3479            assert_eq!(
3480                RetryOptions::parse_from_tags(
3481                    &f,
3482                    Some(&f.rules[1]),
3483                    &f.rules[1].scenarios[3],
3484                    &cli
3485                ),
3486                Some(RetryOptions {
3487                    retries: Retries { current: 0, left: 7 },
3488                    after: Some(Duration::from_secs(3)),
3489                }),
3490            );
3491            assert_eq!(
3492                RetryOptions::parse_from_tags(
3493                    &f,
3494                    Some(&f.rules[1]),
3495                    &f.rules[1].scenarios[4],
3496                    &cli
3497                ),
3498                Some(RetryOptions {
3499                    retries: Retries { current: 0, left: 5 },
3500                    after: Some(Duration::from_secs(15)),
3501                }),
3502            );
3503        }
3504    }
3505}