cucumber/runner/
basic.rs

1// Copyright (c) 2018-2025  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(Some((id, feat, rule, scenario_failed, retried))) =
1038            storage.finished_receiver.try_next()
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    async fn get(
2309        &self,
2310        max_concurrent_scenarios: Option<usize>,
2311    ) -> (
2312        Vec<(
2313            ScenarioId,
2314            Source<gherkin::Feature>,
2315            Option<Source<gherkin::Rule>>,
2316            Source<gherkin::Scenario>,
2317            ScenarioType,
2318            Option<RetryOptions>,
2319        )>,
2320        Option<Duration>,
2321    ) {
2322        use RetryOptionsWithDeadline as WithDeadline;
2323        use ScenarioType::{Concurrent, Serial};
2324
2325        if max_concurrent_scenarios == Some(0) {
2326            return (Vec::new(), None);
2327        }
2328
2329        let mut min_dur = None;
2330        let mut drain =
2331            |storage: &mut Vec<(_, _, _, _, Option<WithDeadline>)>,
2332             ty,
2333             count: Option<usize>| {
2334                let mut i = 0;
2335                let drained = storage
2336                    .extract_if(.., |(_, _, _, _, ret)| {
2337                        // Because of retries involved, we cannot just specify
2338                        // `..count` range to `.extract_if()`.
2339                        if count.filter(|c| i >= *c).is_some() {
2340                            return false;
2341                        }
2342
2343                        ret.as_ref()
2344                            .and_then(WithDeadline::left_until_retry)
2345                            .map_or_else(
2346                                || {
2347                                    i += 1;
2348                                    true
2349                                },
2350                                |left| {
2351                                    min_dur = min_dur
2352                                        .map(|min| cmp::min(min, left))
2353                                        .or(Some(left));
2354                                    false
2355                                },
2356                            )
2357                    })
2358                    .map(|(id, f, r, s, ret)| {
2359                        (id, f, r, s, ty, ret.map(Into::into))
2360                    })
2361                    .collect::<Vec<_>>();
2362                (!drained.is_empty()).then_some(drained)
2363            };
2364
2365        let mut guard = self.scenarios.lock().await;
2366        let scenarios = guard
2367            .get_mut(&Serial)
2368            .and_then(|storage| drain(storage, Serial, Some(1)))
2369            .or_else(|| {
2370                guard.get_mut(&Concurrent).and_then(|storage| {
2371                    drain(storage, Concurrent, max_concurrent_scenarios)
2372                })
2373            })
2374            .unwrap_or_default();
2375
2376        (scenarios, min_dur)
2377    }
2378
2379    /// Marks that there will be no more [`Feature`]s to execute.
2380    ///
2381    /// [`Feature`]: gherkin::Feature
2382    fn finish(&self) {
2383        self.finished.store(true, Ordering::SeqCst);
2384    }
2385
2386    /// Indicates whether there are more [`Feature`]s to execute.
2387    ///
2388    /// `fail_fast` argument indicates whether not yet executed scenarios should
2389    /// be omitted.
2390    ///
2391    /// [`Feature`]: gherkin::Feature
2392    async fn is_finished(&self, fail_fast: bool) -> bool {
2393        self.finished.load(Ordering::SeqCst)
2394            && (fail_fast
2395                || self.scenarios.lock().await.values().all(Vec::is_empty))
2396    }
2397}
2398
2399/// Coerces the given `value` into a type-erased [`Info`].
2400fn coerce_into_info<T: Any + Send + 'static>(val: T) -> Info {
2401    Arc::new(val)
2402}
2403
2404/// Failure encountered during execution of [`HookType::Before`] or [`Step`].
2405/// See [`Executor::emit_failed_events()`] for more info.
2406///
2407/// [`Step`]: gherkin::Step
2408enum ExecutionFailure<World> {
2409    /// [`HookType::Before`] panicked.
2410    BeforeHookPanicked {
2411        /// [`World`] at the time [`HookType::Before`] has panicked.
2412        world: Option<World>,
2413
2414        /// [`catch_unwind()`] of the [`HookType::Before`] panic.
2415        ///
2416        /// [`catch_unwind()`]: std::panic::catch_unwind
2417        panic_info: Info,
2418
2419        /// [`Metadata`] at the time [`HookType::Before`] panicked.
2420        ///
2421        /// [`Metadata`]: event::Metadata
2422        meta: event::Metadata,
2423    },
2424
2425    /// [`Step`] was skipped.
2426    ///
2427    /// [`Step`]: gherkin::Step.
2428    StepSkipped(Option<World>),
2429
2430    /// [`Step`] failed.
2431    ///
2432    /// [`Step`]: gherkin::Step.
2433    StepPanicked {
2434        /// [`World`] at the time when [`Step`] has failed.
2435        ///
2436        /// [`Step`]: gherkin::Step
2437        world: Option<World>,
2438
2439        /// [`Step`] itself.
2440        ///
2441        /// [`Step`]: gherkin::Step
2442        step: Source<gherkin::Step>,
2443
2444        /// [`Step`]s [`regex`] [`CaptureLocations`].
2445        ///
2446        /// [`Step`]: gherkin::Step
2447        captures: Option<CaptureLocations>,
2448
2449        /// [`Location`] of the [`fn`] that matched this [`Step`].
2450        ///
2451        /// [`Location`]: step::Location
2452        /// [`Step`]: gherkin::Step
2453        loc: Option<step::Location>,
2454
2455        /// [`StepError`] of the [`Step`].
2456        ///
2457        /// [`Step`]: gherkin::Step
2458        /// [`StepError`]: event::StepError
2459        err: event::StepError,
2460
2461        /// [`Metadata`] at the time when [`Step`] failed.
2462        ///
2463        /// [`Metadata`]: event::Metadata
2464        /// [`Step`]: gherkin::Step.
2465        meta: event::Metadata,
2466
2467        /// Indicator whether the [`Step`] was background or not.
2468        ///
2469        /// [`Step`]: gherkin::Step
2470        is_background: bool,
2471    },
2472}
2473
2474/// [`Metadata`] of [`HookType::After`] events.
2475///
2476/// [`Metadata`]: event::Metadata
2477struct AfterHookEventsMeta {
2478    /// [`Metadata`] at the time [`HookType::After`] started.
2479    ///
2480    /// [`Metadata`]: event::Metadata
2481    started: event::Metadata,
2482
2483    /// [`Metadata`] at the time [`HookType::After`] finished.
2484    ///
2485    /// [`Metadata`]: event::Metadata
2486    finished: event::Metadata,
2487}
2488
2489impl<W> ExecutionFailure<W> {
2490    /// Takes the [`World`] leaving a [`None`] in its place.
2491    const fn take_world(&mut self) -> Option<W> {
2492        match self {
2493            Self::BeforeHookPanicked { world, .. }
2494            | Self::StepSkipped(world)
2495            | Self::StepPanicked { world, .. } => world.take(),
2496        }
2497    }
2498
2499    /// Creates an [`event::ScenarioFinished`] from this [`ExecutionFailure`].
2500    fn get_scenario_finished_event(&self) -> event::ScenarioFinished {
2501        use event::ScenarioFinished::{
2502            BeforeHookFailed, StepFailed, StepSkipped,
2503        };
2504
2505        match self {
2506            Self::BeforeHookPanicked { panic_info, .. } => {
2507                BeforeHookFailed(Arc::clone(panic_info))
2508            }
2509            Self::StepSkipped(_) => StepSkipped,
2510            Self::StepPanicked { captures, loc, err, .. } => {
2511                StepFailed(captures.clone(), *loc, err.clone())
2512            }
2513        }
2514    }
2515}
2516
2517#[cfg(test)]
2518mod retry_options {
2519    use gherkin::GherkinEnv;
2520    use humantime::parse_duration;
2521
2522    use super::{Cli, Duration, Retries, RetryOptions};
2523
2524    mod scenario_tags {
2525        use super::*;
2526
2527        // language=Gherkin
2528        const FEATURE: &str = r"
2529Feature: only scenarios
2530  Scenario: no tags
2531    Given a step
2532
2533  @retry
2534  Scenario: tag
2535    Given a step
2536
2537  @retry(5)
2538  Scenario: tag with explicit value
2539    Given a step
2540
2541  @retry.after(3s)
2542  Scenario: tag with explicit after
2543    Given a step
2544
2545  @retry(5).after(15s)
2546  Scenario: tag with explicit value and after
2547    Given a step
2548";
2549
2550        #[test]
2551        fn empty_cli() {
2552            let cli = Cli {
2553                concurrency: None,
2554                fail_fast: false,
2555                retry: None,
2556                retry_after: None,
2557                retry_tag_filter: None,
2558            };
2559            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2560                .expect("failed to parse feature");
2561
2562            assert_eq!(
2563                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2564                None,
2565            );
2566            assert_eq!(
2567                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2568                Some(RetryOptions {
2569                    retries: Retries { current: 0, left: 1 },
2570                    after: None,
2571                }),
2572            );
2573            assert_eq!(
2574                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2575                Some(RetryOptions {
2576                    retries: Retries { current: 0, left: 5 },
2577                    after: None,
2578                }),
2579            );
2580            assert_eq!(
2581                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2582                Some(RetryOptions {
2583                    retries: Retries { current: 0, left: 1 },
2584                    after: Some(Duration::from_secs(3)),
2585                }),
2586            );
2587            assert_eq!(
2588                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2589                Some(RetryOptions {
2590                    retries: Retries { current: 0, left: 5 },
2591                    after: Some(Duration::from_secs(15)),
2592                }),
2593            );
2594        }
2595
2596        #[test]
2597        fn cli_retries() {
2598            let cli = Cli {
2599                concurrency: None,
2600                fail_fast: false,
2601                retry: Some(7),
2602                retry_after: None,
2603                retry_tag_filter: None,
2604            };
2605            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2606                .expect("failed to parse feature");
2607
2608            assert_eq!(
2609                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2610                Some(RetryOptions {
2611                    retries: Retries { current: 0, left: 7 },
2612                    after: None,
2613                }),
2614            );
2615            assert_eq!(
2616                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2617                Some(RetryOptions {
2618                    retries: Retries { current: 0, left: 7 },
2619                    after: None,
2620                }),
2621            );
2622            assert_eq!(
2623                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2624                Some(RetryOptions {
2625                    retries: Retries { current: 0, left: 5 },
2626                    after: None,
2627                }),
2628            );
2629            assert_eq!(
2630                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2631                Some(RetryOptions {
2632                    retries: Retries { current: 0, left: 7 },
2633                    after: Some(Duration::from_secs(3)),
2634                }),
2635            );
2636            assert_eq!(
2637                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2638                Some(RetryOptions {
2639                    retries: Retries { current: 0, left: 5 },
2640                    after: Some(Duration::from_secs(15)),
2641                }),
2642            );
2643        }
2644
2645        #[test]
2646        fn cli_retry_after() {
2647            let cli = Cli {
2648                concurrency: None,
2649                fail_fast: false,
2650                retry: Some(7),
2651                retry_after: Some(parse_duration("5s").unwrap()),
2652                retry_tag_filter: None,
2653            };
2654            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2655                .expect("failed to parse feature");
2656
2657            assert_eq!(
2658                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2659                Some(RetryOptions {
2660                    retries: Retries { current: 0, left: 7 },
2661                    after: Some(Duration::from_secs(5)),
2662                }),
2663            );
2664            assert_eq!(
2665                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2666                Some(RetryOptions {
2667                    retries: Retries { current: 0, left: 7 },
2668                    after: Some(Duration::from_secs(5)),
2669                }),
2670            );
2671            assert_eq!(
2672                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2673                Some(RetryOptions {
2674                    retries: Retries { current: 0, left: 5 },
2675                    after: Some(Duration::from_secs(5)),
2676                }),
2677            );
2678            assert_eq!(
2679                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2680                Some(RetryOptions {
2681                    retries: Retries { current: 0, left: 7 },
2682                    after: Some(Duration::from_secs(3)),
2683                }),
2684            );
2685            assert_eq!(
2686                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2687                Some(RetryOptions {
2688                    retries: Retries { current: 0, left: 5 },
2689                    after: Some(Duration::from_secs(15)),
2690                }),
2691            );
2692        }
2693
2694        #[test]
2695        fn cli_retry_filter() {
2696            let cli = Cli {
2697                concurrency: None,
2698                fail_fast: false,
2699                retry: Some(7),
2700                retry_after: None,
2701                retry_tag_filter: Some("@retry".parse().unwrap()),
2702            };
2703            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2704                .expect("failed to parse feature");
2705
2706            assert_eq!(
2707                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2708                None,
2709            );
2710            assert_eq!(
2711                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2712                Some(RetryOptions {
2713                    retries: Retries { current: 0, left: 7 },
2714                    after: None,
2715                }),
2716            );
2717            assert_eq!(
2718                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2719                Some(RetryOptions {
2720                    retries: Retries { current: 0, left: 5 },
2721                    after: None,
2722                }),
2723            );
2724            assert_eq!(
2725                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2726                Some(RetryOptions {
2727                    retries: Retries { current: 0, left: 7 },
2728                    after: Some(Duration::from_secs(3)),
2729                }),
2730            );
2731            assert_eq!(
2732                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2733                Some(RetryOptions {
2734                    retries: Retries { current: 0, left: 5 },
2735                    after: Some(Duration::from_secs(15)),
2736                }),
2737            );
2738        }
2739
2740        #[test]
2741        fn cli_retry_after_and_filter() {
2742            let cli = Cli {
2743                concurrency: None,
2744                fail_fast: false,
2745                retry: Some(7),
2746                retry_after: Some(parse_duration("5s").unwrap()),
2747                retry_tag_filter: Some("@retry".parse().unwrap()),
2748            };
2749            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2750                .expect("failed to parse feature");
2751
2752            assert_eq!(
2753                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2754                None,
2755            );
2756            assert_eq!(
2757                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2758                Some(RetryOptions {
2759                    retries: Retries { current: 0, left: 7 },
2760                    after: Some(Duration::from_secs(5)),
2761                }),
2762            );
2763            assert_eq!(
2764                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2765                Some(RetryOptions {
2766                    retries: Retries { current: 0, left: 5 },
2767                    after: Some(Duration::from_secs(5)),
2768                }),
2769            );
2770            assert_eq!(
2771                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2772                Some(RetryOptions {
2773                    retries: Retries { current: 0, left: 7 },
2774                    after: Some(Duration::from_secs(3)),
2775                }),
2776            );
2777            assert_eq!(
2778                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2779                Some(RetryOptions {
2780                    retries: Retries { current: 0, left: 5 },
2781                    after: Some(Duration::from_secs(15)),
2782                }),
2783            );
2784        }
2785    }
2786
2787    mod rule_tags {
2788        use super::*;
2789
2790        // language=Gherkin
2791        const FEATURE: &str = r#"
2792Feature: only scenarios
2793  Rule: no tags
2794    Scenario: no tags
2795      Given a step
2796
2797    @retry
2798    Scenario: tag
2799      Given a step
2800
2801    @retry(5)
2802    Scenario: tag with explicit value
2803      Given a step
2804
2805    @retry.after(3s)
2806    Scenario: tag with explicit after
2807      Given a step
2808
2809    @retry(5).after(15s)
2810    Scenario: tag with explicit value and after
2811      Given a step
2812
2813  @retry(3).after(5s)
2814  Rule: retry tag
2815    Scenario: no tags
2816      Given a step
2817
2818    @retry
2819    Scenario: tag
2820      Given a step
2821
2822    @retry(5)
2823    Scenario: tag with explicit value
2824      Given a step
2825
2826    @retry.after(3s)
2827    Scenario: tag with explicit after
2828      Given a step
2829
2830    @retry(5).after(15s)
2831    Scenario: tag with explicit value and after
2832      Given a step
2833"#;
2834
2835        #[test]
2836        fn empty_cli() {
2837            let cli = Cli {
2838                concurrency: None,
2839                fail_fast: false,
2840                retry: None,
2841                retry_after: None,
2842                retry_tag_filter: None,
2843            };
2844            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2845                .expect("failed to parse feature");
2846
2847            assert_eq!(
2848                RetryOptions::parse_from_tags(
2849                    &f,
2850                    Some(&f.rules[0]),
2851                    &f.rules[0].scenarios[0],
2852                    &cli
2853                ),
2854                None,
2855            );
2856            assert_eq!(
2857                RetryOptions::parse_from_tags(
2858                    &f,
2859                    Some(&f.rules[0]),
2860                    &f.rules[0].scenarios[1],
2861                    &cli
2862                ),
2863                Some(RetryOptions {
2864                    retries: Retries { current: 0, left: 1 },
2865                    after: None,
2866                }),
2867            );
2868            assert_eq!(
2869                RetryOptions::parse_from_tags(
2870                    &f,
2871                    Some(&f.rules[0]),
2872                    &f.rules[0].scenarios[2],
2873                    &cli
2874                ),
2875                Some(RetryOptions {
2876                    retries: Retries { current: 0, left: 5 },
2877                    after: None,
2878                }),
2879            );
2880            assert_eq!(
2881                RetryOptions::parse_from_tags(
2882                    &f,
2883                    Some(&f.rules[0]),
2884                    &f.rules[0].scenarios[3],
2885                    &cli
2886                ),
2887                Some(RetryOptions {
2888                    retries: Retries { current: 0, left: 1 },
2889                    after: Some(Duration::from_secs(3)),
2890                }),
2891            );
2892            assert_eq!(
2893                RetryOptions::parse_from_tags(
2894                    &f,
2895                    Some(&f.rules[0]),
2896                    &f.rules[0].scenarios[4],
2897                    &cli
2898                ),
2899                Some(RetryOptions {
2900                    retries: Retries { current: 0, left: 5 },
2901                    after: Some(Duration::from_secs(15)),
2902                }),
2903            );
2904            assert_eq!(
2905                RetryOptions::parse_from_tags(
2906                    &f,
2907                    Some(&f.rules[1]),
2908                    &f.rules[1].scenarios[0],
2909                    &cli
2910                ),
2911                Some(RetryOptions {
2912                    retries: Retries { current: 0, left: 3 },
2913                    after: Some(Duration::from_secs(5)),
2914                }),
2915            );
2916            assert_eq!(
2917                RetryOptions::parse_from_tags(
2918                    &f,
2919                    Some(&f.rules[1]),
2920                    &f.rules[1].scenarios[1],
2921                    &cli
2922                ),
2923                Some(RetryOptions {
2924                    retries: Retries { current: 0, left: 1 },
2925                    after: None,
2926                }),
2927            );
2928            assert_eq!(
2929                RetryOptions::parse_from_tags(
2930                    &f,
2931                    Some(&f.rules[1]),
2932                    &f.rules[1].scenarios[2],
2933                    &cli
2934                ),
2935                Some(RetryOptions {
2936                    retries: Retries { current: 0, left: 5 },
2937                    after: None,
2938                }),
2939            );
2940            assert_eq!(
2941                RetryOptions::parse_from_tags(
2942                    &f,
2943                    Some(&f.rules[1]),
2944                    &f.rules[1].scenarios[3],
2945                    &cli
2946                ),
2947                Some(RetryOptions {
2948                    retries: Retries { current: 0, left: 1 },
2949                    after: Some(Duration::from_secs(3)),
2950                }),
2951            );
2952            assert_eq!(
2953                RetryOptions::parse_from_tags(
2954                    &f,
2955                    Some(&f.rules[1]),
2956                    &f.rules[1].scenarios[4],
2957                    &cli
2958                ),
2959                Some(RetryOptions {
2960                    retries: Retries { current: 0, left: 5 },
2961                    after: Some(Duration::from_secs(15)),
2962                }),
2963            );
2964        }
2965
2966        #[test]
2967        fn cli_retry_after_and_filter() {
2968            let cli = Cli {
2969                concurrency: None,
2970                fail_fast: false,
2971                retry: Some(7),
2972                retry_after: Some(parse_duration("5s").unwrap()),
2973                retry_tag_filter: Some("@retry".parse().unwrap()),
2974            };
2975            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2976                .expect("failed to parse feature");
2977
2978            assert_eq!(
2979                RetryOptions::parse_from_tags(
2980                    &f,
2981                    Some(&f.rules[0]),
2982                    &f.rules[0].scenarios[0],
2983                    &cli
2984                ),
2985                None,
2986            );
2987            assert_eq!(
2988                RetryOptions::parse_from_tags(
2989                    &f,
2990                    Some(&f.rules[0]),
2991                    &f.rules[0].scenarios[1],
2992                    &cli
2993                ),
2994                Some(RetryOptions {
2995                    retries: Retries { current: 0, left: 7 },
2996                    after: Some(Duration::from_secs(5)),
2997                }),
2998            );
2999            assert_eq!(
3000                RetryOptions::parse_from_tags(
3001                    &f,
3002                    Some(&f.rules[0]),
3003                    &f.rules[0].scenarios[2],
3004                    &cli
3005                ),
3006                Some(RetryOptions {
3007                    retries: Retries { current: 0, left: 5 },
3008                    after: Some(Duration::from_secs(5)),
3009                }),
3010            );
3011            assert_eq!(
3012                RetryOptions::parse_from_tags(
3013                    &f,
3014                    Some(&f.rules[0]),
3015                    &f.rules[0].scenarios[3],
3016                    &cli
3017                ),
3018                Some(RetryOptions {
3019                    retries: Retries { current: 0, left: 7 },
3020                    after: Some(Duration::from_secs(3)),
3021                }),
3022            );
3023            assert_eq!(
3024                RetryOptions::parse_from_tags(
3025                    &f,
3026                    Some(&f.rules[0]),
3027                    &f.rules[0].scenarios[4],
3028                    &cli
3029                ),
3030                Some(RetryOptions {
3031                    retries: Retries { current: 0, left: 5 },
3032                    after: Some(Duration::from_secs(15)),
3033                }),
3034            );
3035            assert_eq!(
3036                RetryOptions::parse_from_tags(
3037                    &f,
3038                    Some(&f.rules[1]),
3039                    &f.rules[1].scenarios[0],
3040                    &cli
3041                ),
3042                Some(RetryOptions {
3043                    retries: Retries { current: 0, left: 3 },
3044                    after: Some(Duration::from_secs(5)),
3045                }),
3046            );
3047            assert_eq!(
3048                RetryOptions::parse_from_tags(
3049                    &f,
3050                    Some(&f.rules[1]),
3051                    &f.rules[1].scenarios[1],
3052                    &cli
3053                ),
3054                Some(RetryOptions {
3055                    retries: Retries { current: 0, left: 7 },
3056                    after: Some(Duration::from_secs(5)),
3057                }),
3058            );
3059            assert_eq!(
3060                RetryOptions::parse_from_tags(
3061                    &f,
3062                    Some(&f.rules[1]),
3063                    &f.rules[1].scenarios[2],
3064                    &cli
3065                ),
3066                Some(RetryOptions {
3067                    retries: Retries { current: 0, left: 5 },
3068                    after: Some(Duration::from_secs(5)),
3069                }),
3070            );
3071            assert_eq!(
3072                RetryOptions::parse_from_tags(
3073                    &f,
3074                    Some(&f.rules[1]),
3075                    &f.rules[1].scenarios[3],
3076                    &cli
3077                ),
3078                Some(RetryOptions {
3079                    retries: Retries { current: 0, left: 7 },
3080                    after: Some(Duration::from_secs(3)),
3081                }),
3082            );
3083            assert_eq!(
3084                RetryOptions::parse_from_tags(
3085                    &f,
3086                    Some(&f.rules[1]),
3087                    &f.rules[1].scenarios[4],
3088                    &cli
3089                ),
3090                Some(RetryOptions {
3091                    retries: Retries { current: 0, left: 5 },
3092                    after: Some(Duration::from_secs(15)),
3093                }),
3094            );
3095        }
3096    }
3097
3098    mod feature_tags {
3099        use super::*;
3100
3101        // language=Gherkin
3102        const FEATURE: &str = r"
3103@retry(8)
3104Feature: only scenarios
3105  Scenario: no tags
3106    Given a step
3107
3108  @retry
3109  Scenario: tag
3110    Given a step
3111
3112  @retry(5)
3113  Scenario: tag with explicit value
3114    Given a step
3115
3116  @retry.after(3s)
3117  Scenario: tag with explicit after
3118    Given a step
3119
3120  @retry(5).after(15s)
3121  Scenario: tag with explicit value and after
3122    Given a step
3123
3124  Rule: no tags
3125    Scenario: no tags
3126      Given a step
3127
3128    @retry
3129    Scenario: tag
3130      Given a step
3131
3132    @retry(5)
3133    Scenario: tag with explicit value
3134      Given a step
3135
3136    @retry.after(3s)
3137    Scenario: tag with explicit after
3138      Given a step
3139
3140    @retry(5).after(15s)
3141    Scenario: tag with explicit value and after
3142      Given a step
3143
3144  @retry(3).after(5s)
3145  Rule: retry tag
3146    Scenario: no tags
3147      Given a step
3148
3149    @retry
3150    Scenario: tag
3151      Given a step
3152
3153    @retry(5)
3154    Scenario: tag with explicit value
3155      Given a step
3156
3157    @retry.after(3s)
3158    Scenario: tag with explicit after
3159      Given a step
3160
3161    @retry(5).after(15s)
3162    Scenario: tag with explicit value and after
3163      Given a step
3164";
3165
3166        #[test]
3167        fn empty_cli() {
3168            let cli = Cli {
3169                concurrency: None,
3170                fail_fast: false,
3171                retry: None,
3172                retry_after: None,
3173                retry_tag_filter: None,
3174            };
3175            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3176                .unwrap_or_else(|e| panic!("failed to parse feature: {e}"));
3177
3178            assert_eq!(
3179                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3180                Some(RetryOptions {
3181                    retries: Retries { current: 0, left: 8 },
3182                    after: None,
3183                }),
3184            );
3185            assert_eq!(
3186                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3187                Some(RetryOptions {
3188                    retries: Retries { current: 0, left: 1 },
3189                    after: None,
3190                }),
3191            );
3192            assert_eq!(
3193                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3194                Some(RetryOptions {
3195                    retries: Retries { current: 0, left: 5 },
3196                    after: None,
3197                }),
3198            );
3199            assert_eq!(
3200                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3201                Some(RetryOptions {
3202                    retries: Retries { current: 0, left: 1 },
3203                    after: Some(Duration::from_secs(3)),
3204                }),
3205            );
3206            assert_eq!(
3207                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3208                Some(RetryOptions {
3209                    retries: Retries { current: 0, left: 5 },
3210                    after: Some(Duration::from_secs(15)),
3211                }),
3212            );
3213            assert_eq!(
3214                RetryOptions::parse_from_tags(
3215                    &f,
3216                    Some(&f.rules[0]),
3217                    &f.rules[0].scenarios[0],
3218                    &cli
3219                ),
3220                Some(RetryOptions {
3221                    retries: Retries { current: 0, left: 8 },
3222                    after: None,
3223                }),
3224            );
3225            assert_eq!(
3226                RetryOptions::parse_from_tags(
3227                    &f,
3228                    Some(&f.rules[0]),
3229                    &f.rules[0].scenarios[1],
3230                    &cli
3231                ),
3232                Some(RetryOptions {
3233                    retries: Retries { current: 0, left: 1 },
3234                    after: None,
3235                }),
3236            );
3237            assert_eq!(
3238                RetryOptions::parse_from_tags(
3239                    &f,
3240                    Some(&f.rules[0]),
3241                    &f.rules[0].scenarios[2],
3242                    &cli
3243                ),
3244                Some(RetryOptions {
3245                    retries: Retries { current: 0, left: 5 },
3246                    after: None,
3247                }),
3248            );
3249            assert_eq!(
3250                RetryOptions::parse_from_tags(
3251                    &f,
3252                    Some(&f.rules[0]),
3253                    &f.rules[0].scenarios[3],
3254                    &cli
3255                ),
3256                Some(RetryOptions {
3257                    retries: Retries { current: 0, left: 1 },
3258                    after: Some(Duration::from_secs(3)),
3259                }),
3260            );
3261            assert_eq!(
3262                RetryOptions::parse_from_tags(
3263                    &f,
3264                    Some(&f.rules[0]),
3265                    &f.rules[0].scenarios[4],
3266                    &cli
3267                ),
3268                Some(RetryOptions {
3269                    retries: Retries { current: 0, left: 5 },
3270                    after: Some(Duration::from_secs(15)),
3271                }),
3272            );
3273            assert_eq!(
3274                RetryOptions::parse_from_tags(
3275                    &f,
3276                    Some(&f.rules[1]),
3277                    &f.rules[1].scenarios[0],
3278                    &cli
3279                ),
3280                Some(RetryOptions {
3281                    retries: Retries { current: 0, left: 3 },
3282                    after: Some(Duration::from_secs(5)),
3283                }),
3284            );
3285            assert_eq!(
3286                RetryOptions::parse_from_tags(
3287                    &f,
3288                    Some(&f.rules[1]),
3289                    &f.rules[1].scenarios[1],
3290                    &cli
3291                ),
3292                Some(RetryOptions {
3293                    retries: Retries { current: 0, left: 1 },
3294                    after: None,
3295                }),
3296            );
3297            assert_eq!(
3298                RetryOptions::parse_from_tags(
3299                    &f,
3300                    Some(&f.rules[1]),
3301                    &f.rules[1].scenarios[2],
3302                    &cli
3303                ),
3304                Some(RetryOptions {
3305                    retries: Retries { current: 0, left: 5 },
3306                    after: None,
3307                }),
3308            );
3309            assert_eq!(
3310                RetryOptions::parse_from_tags(
3311                    &f,
3312                    Some(&f.rules[1]),
3313                    &f.rules[1].scenarios[3],
3314                    &cli
3315                ),
3316                Some(RetryOptions {
3317                    retries: Retries { current: 0, left: 1 },
3318                    after: Some(Duration::from_secs(3)),
3319                }),
3320            );
3321            assert_eq!(
3322                RetryOptions::parse_from_tags(
3323                    &f,
3324                    Some(&f.rules[1]),
3325                    &f.rules[1].scenarios[4],
3326                    &cli
3327                ),
3328                Some(RetryOptions {
3329                    retries: Retries { current: 0, left: 5 },
3330                    after: Some(Duration::from_secs(15)),
3331                }),
3332            );
3333        }
3334
3335        #[test]
3336        fn cli_retry_after_and_filter() {
3337            let cli = Cli {
3338                concurrency: None,
3339                fail_fast: false,
3340                retry: Some(7),
3341                retry_after: Some(parse_duration("5s").unwrap()),
3342                retry_tag_filter: Some("@retry".parse().unwrap()),
3343            };
3344            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3345                .expect("failed to parse feature");
3346
3347            assert_eq!(
3348                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3349                Some(RetryOptions {
3350                    retries: Retries { current: 0, left: 8 },
3351                    after: Some(Duration::from_secs(5)),
3352                }),
3353            );
3354            assert_eq!(
3355                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3356                Some(RetryOptions {
3357                    retries: Retries { current: 0, left: 7 },
3358                    after: Some(Duration::from_secs(5)),
3359                }),
3360            );
3361            assert_eq!(
3362                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3363                Some(RetryOptions {
3364                    retries: Retries { current: 0, left: 5 },
3365                    after: Some(Duration::from_secs(5)),
3366                }),
3367            );
3368            assert_eq!(
3369                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3370                Some(RetryOptions {
3371                    retries: Retries { current: 0, left: 7 },
3372                    after: Some(Duration::from_secs(3)),
3373                }),
3374            );
3375            assert_eq!(
3376                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3377                Some(RetryOptions {
3378                    retries: Retries { current: 0, left: 5 },
3379                    after: Some(Duration::from_secs(15)),
3380                }),
3381            );
3382            assert_eq!(
3383                RetryOptions::parse_from_tags(
3384                    &f,
3385                    Some(&f.rules[0]),
3386                    &f.rules[0].scenarios[0],
3387                    &cli
3388                ),
3389                Some(RetryOptions {
3390                    retries: Retries { current: 0, left: 8 },
3391                    after: Some(Duration::from_secs(5)),
3392                }),
3393            );
3394            assert_eq!(
3395                RetryOptions::parse_from_tags(
3396                    &f,
3397                    Some(&f.rules[0]),
3398                    &f.rules[0].scenarios[1],
3399                    &cli
3400                ),
3401                Some(RetryOptions {
3402                    retries: Retries { current: 0, left: 7 },
3403                    after: Some(Duration::from_secs(5)),
3404                }),
3405            );
3406            assert_eq!(
3407                RetryOptions::parse_from_tags(
3408                    &f,
3409                    Some(&f.rules[0]),
3410                    &f.rules[0].scenarios[2],
3411                    &cli
3412                ),
3413                Some(RetryOptions {
3414                    retries: Retries { current: 0, left: 5 },
3415                    after: Some(Duration::from_secs(5)),
3416                }),
3417            );
3418            assert_eq!(
3419                RetryOptions::parse_from_tags(
3420                    &f,
3421                    Some(&f.rules[0]),
3422                    &f.rules[0].scenarios[3],
3423                    &cli
3424                ),
3425                Some(RetryOptions {
3426                    retries: Retries { current: 0, left: 7 },
3427                    after: Some(Duration::from_secs(3)),
3428                }),
3429            );
3430            assert_eq!(
3431                RetryOptions::parse_from_tags(
3432                    &f,
3433                    Some(&f.rules[0]),
3434                    &f.rules[0].scenarios[4],
3435                    &cli
3436                ),
3437                Some(RetryOptions {
3438                    retries: Retries { current: 0, left: 5 },
3439                    after: Some(Duration::from_secs(15)),
3440                }),
3441            );
3442            assert_eq!(
3443                RetryOptions::parse_from_tags(
3444                    &f,
3445                    Some(&f.rules[1]),
3446                    &f.rules[1].scenarios[0],
3447                    &cli
3448                ),
3449                Some(RetryOptions {
3450                    retries: Retries { current: 0, left: 3 },
3451                    after: Some(Duration::from_secs(5)),
3452                }),
3453            );
3454            assert_eq!(
3455                RetryOptions::parse_from_tags(
3456                    &f,
3457                    Some(&f.rules[1]),
3458                    &f.rules[1].scenarios[1],
3459                    &cli
3460                ),
3461                Some(RetryOptions {
3462                    retries: Retries { current: 0, left: 7 },
3463                    after: Some(Duration::from_secs(5)),
3464                }),
3465            );
3466            assert_eq!(
3467                RetryOptions::parse_from_tags(
3468                    &f,
3469                    Some(&f.rules[1]),
3470                    &f.rules[1].scenarios[2],
3471                    &cli
3472                ),
3473                Some(RetryOptions {
3474                    retries: Retries { current: 0, left: 5 },
3475                    after: Some(Duration::from_secs(5)),
3476                }),
3477            );
3478            assert_eq!(
3479                RetryOptions::parse_from_tags(
3480                    &f,
3481                    Some(&f.rules[1]),
3482                    &f.rules[1].scenarios[3],
3483                    &cli
3484                ),
3485                Some(RetryOptions {
3486                    retries: Retries { current: 0, left: 7 },
3487                    after: Some(Duration::from_secs(3)),
3488                }),
3489            );
3490            assert_eq!(
3491                RetryOptions::parse_from_tags(
3492                    &f,
3493                    Some(&f.rules[1]),
3494                    &f.rules[1].scenarios[4],
3495                    &cli
3496                ),
3497                Some(RetryOptions {
3498                    retries: Retries { current: 0, left: 5 },
3499                    after: Some(Duration::from_secs(15)),
3500                }),
3501            );
3502        }
3503    }
3504}