cucumber 0.19.1

Cucumber testing framework for Rust, with async support. Fully native, no external test runners or dependencies.
Documentation
// Copyright (c) 2018-2022  Brendan Molloy <brendan@bbqsrc.net>,
//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
//                          Kai Ren <tyranron@gmail.com>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Default [`Runner`] implementation.

use std::{
    cmp,
    collections::HashMap,
    fmt, iter, mem,
    ops::ControlFlow,
    panic::{self, AssertUnwindSafe},
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    thread,
    time::{Duration, Instant},
};

use drain_filter_polyfill::VecExt;
use futures::{
    channel::{mpsc, oneshot},
    future::{self, Either, LocalBoxFuture},
    lock::Mutex,
    pin_mut,
    stream::{self, LocalBoxStream},
    FutureExt as _, Stream, StreamExt as _, TryFutureExt as _,
    TryStreamExt as _,
};
use gherkin::tagexpr::TagOperation;
use itertools::Itertools as _;
use regex::{CaptureLocations, Regex};

use crate::{
    event::{self, HookType, Info, Retries},
    feature::Ext as _,
    future::yield_now,
    parser, step,
    tag::Ext as _,
    Event, Runner, Step, World,
};

/// CLI options of a [`Basic`] [`Runner`].
#[derive(clap::Args, Clone, Debug, Default)]
#[group(skip)]
pub struct Cli {
    /// Number of scenarios to run concurrently. If not specified, uses the
    /// value configured in tests runner, or 64 by default.
    #[arg(long, short, value_name = "int", global = true)]
    pub concurrency: Option<usize>,

    /// Run tests until the first failure.
    #[arg(long, global = true, visible_alias = "ff")]
    pub fail_fast: bool,

    /// Number of times a scenario will be retried in case of a failure.
    #[arg(long, value_name = "int", global = true)]
    pub retry: Option<usize>,

    /// Delay between each scenario retry attempt.
    ///
    /// Duration is represented in a human-readable format like `12min5s`.
    /// Supported suffixes:
    /// - `nsec`, `ns` — nanoseconds.
    /// - `usec`, `us` — microseconds.
    /// - `msec`, `ms` — milliseconds.
    /// - `seconds`, `second`, `sec`, `s` - seconds.
    /// - `minutes`, `minute`, `min`, `m` - minutes.
    #[arg(
        long,
        value_name = "duration",
        value_parser = humantime::parse_duration,
        verbatim_doc_comment,
        global = true,
    )]
    pub retry_after: Option<Duration>,

    /// Tag expression to filter retried scenarios.
    #[arg(long, value_name = "tagexpr", global = true)]
    pub retry_tag_filter: Option<TagOperation>,
}

/// Type determining whether [`Scenario`]s should run concurrently or
/// sequentially.
///
/// [`Scenario`]: gherkin::Scenario
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ScenarioType {
    /// Run [`Scenario`]s sequentially (one-by-one).
    ///
    /// [`Scenario`]: gherkin::Scenario
    Serial,

    /// Run [`Scenario`]s concurrently.
    ///
    /// [`Scenario`]: gherkin::Scenario
    Concurrent,
}

/// Options for retrying [`Scenario`]s.
///
/// [`Scenario`]: gherkin::Scenario
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetryOptions {
    /// Number of [`Retries`].
    pub retries: Retries,

    /// Delay before next retry attempt will be executed.
    pub after: Option<Duration>,
}

impl RetryOptions {
    /// Returns [`Some`], in case next retry attempt is available, or [`None`]
    /// otherwise.
    #[must_use]
    pub fn next_try(self) -> Option<Self> {
        self.retries.next_try().map(|num| Self {
            retries: num,
            after: self.after,
        })
    }

    /// Parses [`RetryOptions`] from [`Feature`]'s, [`Rule`]'s, [`Scenario`]'s
    /// tags and [`Cli`] options.
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Rule`]: gherkin::Rule
    /// [`Scenario`]: gherkin::Scenario
    #[must_use]
    pub fn parse_from_tags(
        feature: &gherkin::Feature,
        rule: Option<&gherkin::Rule>,
        scenario: &gherkin::Scenario,
        cli: &Cli,
    ) -> Option<Self> {
        #[allow(clippy::shadow_unrelated)]
        let parse_tags = |tags: &[String]| {
            tags.iter().find_map(|tag| {
                tag.strip_prefix("retry").map(|retries| {
                    let (num, rest) = retries
                        .strip_prefix('(')
                        .and_then(|s| {
                            s.split_once(')').and_then(|(num, rest)| {
                                num.parse::<usize>()
                                    .ok()
                                    .map(|num| (Some(num), rest))
                            })
                        })
                        .unwrap_or((None, retries));

                    let after = rest.strip_prefix(".after").and_then(|after| {
                        after.strip_prefix('(').and_then(|after| {
                            let (dur, _) = after.split_once(')')?;
                            humantime::parse_duration(dur).ok()
                        })
                    });

                    (num, after)
                })
            })
        };

        let apply_cli = |options: Option<_>| {
            let matched = cli.retry_tag_filter.as_ref().map_or_else(
                || cli.retry.is_some() || cli.retry_after.is_some(),
                |op| {
                    op.eval(scenario.tags.iter().chain(
                        rule.iter().flat_map(|r| &r.tags).chain(&feature.tags),
                    ))
                },
            );

            (options.is_some() || matched).then(|| Self {
                retries: Retries::initial(
                    options.and_then(|(r, _)| r).or(cli.retry).unwrap_or(1),
                ),
                after: options.and_then(|(_, a)| a).or(cli.retry_after),
            })
        };

        apply_cli(
            parse_tags(&scenario.tags)
                .or_else(|| rule.and_then(|r| parse_tags(&r.tags)))
                .or_else(|| parse_tags(&feature.tags)),
        )
    }

    /// Constructs [`RetryOptionsWithDeadline`], that will reschedule
    /// [`Scenario`] [`after`] delay.
    ///
    /// [`after`]: RetryOptions::after
    /// [`Scenario`]: gherkin::Scenario
    fn with_deadline(self, now: Instant) -> RetryOptionsWithDeadline {
        RetryOptionsWithDeadline {
            retries: self.retries,
            after: self.after.map(|at| (at, Some(now))),
        }
    }

    /// Constructs [`RetryOptionsWithDeadline`], that will reschedule
    /// [`Scenario`] immediately, ignoring [`RetryOptions::after`]. Used for
    /// initial [`Scenario`] run, where we don't need to wait for the delay.
    ///
    /// [`Scenario`]: gherkin::Scenario
    fn without_deadline(self) -> RetryOptionsWithDeadline {
        RetryOptionsWithDeadline {
            retries: self.retries,
            after: self.after.map(|at| (at, None)),
        }
    }
}

/// [`RetryOptions`] with an [`Option`]al [`Instant`] to determine, whether
/// [`Scenario`] should be already rescheduled or not.
///
/// [`Scenario`]: gherkin::Scenario
#[derive(Clone, Copy, Debug)]
pub struct RetryOptionsWithDeadline {
    /// Number of [`Retries`].
    pub retries: Retries,

    /// Delay before next retry attempt will be executed.
    pub after: Option<(Duration, Option<Instant>)>,
}

impl From<RetryOptionsWithDeadline> for RetryOptions {
    fn from(v: RetryOptionsWithDeadline) -> Self {
        Self {
            retries: v.retries,
            after: v.after.map(|(at, _)| at),
        }
    }
}

impl RetryOptionsWithDeadline {
    /// Returns [`Duration`] after which a [`Scenario`] could be retried. If
    /// [`None`], then [`Scenario`] is ready for the retry.
    ///
    /// [`Scenario`]: gherkin::Scenario
    fn left_until_retry(&self) -> Option<Duration> {
        let (dur, instant) = self.after?;
        dur.checked_sub(instant?.elapsed())
    }
}

/// Alias for [`fn`] used to determine whether a [`Scenario`] is [`Concurrent`]
/// or a [`Serial`] one.
///
/// [`Concurrent`]: ScenarioType::Concurrent
/// [`Serial`]: ScenarioType::Serial
/// [`Scenario`]: gherkin::Scenario
pub type WhichScenarioFn = fn(
    &gherkin::Feature,
    Option<&gherkin::Rule>,
    &gherkin::Scenario,
) -> ScenarioType;

/// Alias for [`Arc`]ed [`Fn`] used to determine [`Scenario`]'s
/// [`RetryOptions`].
///
/// [`Scenario`]: gherkin::Scenario
pub type RetryOptionsFn = Arc<
    dyn Fn(
        &gherkin::Feature,
        Option<&gherkin::Rule>,
        &gherkin::Scenario,
        &Cli,
    ) -> Option<RetryOptions>,
>;

/// Alias for [`fn`] executed on each [`Scenario`] before running all [`Step`]s.
///
/// [`Scenario`]: gherkin::Scenario
/// [`Step`]: gherkin::Step
pub type BeforeHookFn<World> = for<'a> fn(
    &'a gherkin::Feature,
    Option<&'a gherkin::Rule>,
    &'a gherkin::Scenario,
    &'a mut World,
) -> LocalBoxFuture<'a, ()>;

/// Alias for [`fn`] executed on each [`Scenario`] after running all [`Step`]s.
///
/// [`Scenario`]: gherkin::Scenario
/// [`Step`]: gherkin::Step
pub type AfterHookFn<World> = for<'a> fn(
    &'a gherkin::Feature,
    Option<&'a gherkin::Rule>,
    &'a gherkin::Scenario,
    &'a event::ScenarioFinished,
    Option<&'a mut World>,
) -> LocalBoxFuture<'a, ()>;

/// Alias for a failed [`Scenario`].
///
/// [`Scenario`]: gherkin::Scenario
type IsFailed = bool;

/// Alias for a retried [`Scenario`].
///
/// [`Scenario`]: gherkin::Scenario
type IsRetried = bool;

/// Default [`Runner`] implementation which follows [_order guarantees_][1] from
/// the [`Runner`] trait docs.
///
/// Executes [`Scenario`]s concurrently based on the custom function, which
/// returns [`ScenarioType`]. Also, can limit maximum number of concurrent
/// [`Scenario`]s.
///
/// [1]: Runner#order-guarantees
/// [`Scenario`]: gherkin::Scenario
pub struct Basic<
    World,
    F = WhichScenarioFn,
    Before = BeforeHookFn<World>,
    After = AfterHookFn<World>,
> {
    /// Optional number of concurrently executed [`Scenario`]s.
    ///
    /// [`Scenario`]: gherkin::Scenario
    max_concurrent_scenarios: Option<usize>,

    /// Optional number of retries of failed [`Scenario`]s.
    ///
    /// [`Scenario`]: gherkin::Scenario
    retries: Option<usize>,

    /// Optional [`Duration`] between retries of failed [`Scenario`]s.
    ///
    /// [`Scenario`]: gherkin::Scenario
    retry_after: Option<Duration>,

    /// Optional [`TagOperation`] filter for retries of failed [`Scenario`]s.
    ///
    /// [`Scenario`]: gherkin::Scenario
    retry_filter: Option<TagOperation>,

    /// [`Collection`] of functions to match [`Step`]s.
    ///
    /// [`Collection`]: step::Collection
    steps: step::Collection<World>,

    /// Function determining whether a [`Scenario`] is [`Concurrent`] or
    /// a [`Serial`] one.
    ///
    /// [`Concurrent`]: ScenarioType::Concurrent
    /// [`Serial`]: ScenarioType::Serial
    /// [`Scenario`]: gherkin::Scenario
    which_scenario: F,

    /// Function determining [`Scenario`]'s [`RetryOptions`].
    ///
    /// [`Scenario`]: gherkin::Scenario
    retry_options: RetryOptionsFn,

    /// Function, executed on each [`Scenario`] before running all [`Step`]s,
    /// including [`Background`] ones.
    ///
    /// [`Background`]: gherkin::Background
    /// [`Scenario`]: gherkin::Scenario
    /// [`Step`]: gherkin::Step
    before_hook: Option<Before>,

    /// Function, executed on each [`Scenario`] after running all [`Step`]s.
    ///
    /// [`Background`]: gherkin::Background
    /// [`Scenario`]: gherkin::Scenario
    /// [`Step`]: gherkin::Step
    after_hook: Option<After>,

    /// Indicates whether execution should be stopped after the first failure.
    fail_fast: bool,
}

// Implemented manually to omit redundant `World: Clone` trait bound, imposed by
// `#[derive(Clone)]`.
impl<World, F: Clone, B: Clone, A: Clone> Clone for Basic<World, F, B, A> {
    fn clone(&self) -> Self {
        Self {
            max_concurrent_scenarios: self.max_concurrent_scenarios,
            retries: self.retries,
            retry_after: self.retry_after,
            retry_filter: self.retry_filter.clone(),
            steps: self.steps.clone(),
            which_scenario: self.which_scenario.clone(),
            retry_options: Arc::clone(&self.retry_options),
            before_hook: self.before_hook.clone(),
            after_hook: self.after_hook.clone(),
            fail_fast: self.fail_fast,
        }
    }
}

// Implemented manually to omit redundant trait bounds on `World` and to omit
// outputting `F`.
impl<World, F, B, A> fmt::Debug for Basic<World, F, B, A> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Basic")
            .field("max_concurrent_scenarios", &self.max_concurrent_scenarios)
            .field("retries", &self.retries)
            .field("retry_after", &self.retry_after)
            .field("retry_filter", &self.retry_filter)
            .field("steps", &self.steps)
            .field("fail_fast", &self.fail_fast)
            .finish_non_exhaustive()
    }
}

impl<World> Default for Basic<World> {
    fn default() -> Self {
        let which_scenario: WhichScenarioFn = |feature, rule, scenario| {
            scenario
                .tags
                .iter()
                .chain(rule.iter().flat_map(|r| &r.tags))
                .chain(&feature.tags)
                .find(|tag| *tag == "serial")
                .map_or(ScenarioType::Concurrent, |_| ScenarioType::Serial)
        };

        Self {
            max_concurrent_scenarios: Some(64),
            retries: None,
            retry_after: None,
            retry_filter: None,
            steps: step::Collection::new(),
            which_scenario,
            retry_options: Arc::new(RetryOptions::parse_from_tags),
            before_hook: None,
            after_hook: None,
            fail_fast: false,
        }
    }
}

impl<World, Which, Before, After> Basic<World, Which, Before, After> {
    /// If `max` is [`Some`], then number of concurrently executed [`Scenario`]s
    /// will be limited.
    ///
    /// [`Scenario`]: gherkin::Scenario
    #[must_use]
    pub fn max_concurrent_scenarios(
        mut self,
        max: impl Into<Option<usize>>,
    ) -> Self {
        self.max_concurrent_scenarios = max.into();
        self
    }

    /// If `retries` is [`Some`], then failed [`Scenario`]s will be retried
    /// specified number of times.
    ///
    /// [`Scenario`]: gherkin::Scenario
    #[must_use]
    pub fn retries(mut self, retries: impl Into<Option<usize>>) -> Self {
        self.retries = retries.into();
        self
    }

    /// If `after` is [`Some`], then failed [`Scenario`]s will be retried after
    /// the specified [`Duration`].
    ///
    /// [`Scenario`]: gherkin::Scenario
    #[must_use]
    pub fn retry_after(mut self, after: impl Into<Option<Duration>>) -> Self {
        self.retry_after = after.into();
        self
    }

    /// If `filter` is [`Some`], then failed [`Scenario`]s will be retried only
    /// if they're matching the specified `tag_expression`.
    ///
    /// [`Scenario`]: gherkin::Scenario
    #[must_use]
    pub fn retry_filter(
        mut self,
        tag_expression: impl Into<Option<TagOperation>>,
    ) -> Self {
        self.retry_filter = tag_expression.into();
        self
    }

    /// Makes stop running tests on the first failure.
    ///
    /// __NOTE__: All the already started [`Scenario`]s at the moment of failure
    ///           will be finished.
    ///
    /// __NOTE__: Retried [`Scenario`]s are considered as failed, only in case
    ///           they exhaust all retry attempts and still fail.
    ///
    /// [`Scenario`]: gherkin::Scenario
    #[must_use]
    pub const fn fail_fast(mut self) -> Self {
        self.fail_fast = true;
        self
    }

    /// Function determining whether a [`Scenario`] is [`Concurrent`] or
    /// a [`Serial`] one.
    ///
    /// [`Concurrent`]: ScenarioType::Concurrent
    /// [`Serial`]: ScenarioType::Serial
    /// [`Scenario`]: gherkin::Scenario
    #[allow(clippy::missing_const_for_fn)] // false positive: drop in const
    #[must_use]
    pub fn which_scenario<F>(self, func: F) -> Basic<World, F, Before, After>
    where
        F: Fn(
                &gherkin::Feature,
                Option<&gherkin::Rule>,
                &gherkin::Scenario,
            ) -> ScenarioType
            + 'static,
    {
        let Self {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            retry_options,
            before_hook,
            after_hook,
            fail_fast,
            ..
        } = self;
        Basic {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            which_scenario: func,
            retry_options,
            before_hook,
            after_hook,
            fail_fast,
        }
    }

    /// Function determining [`Scenario`]'s [`RetryOptions`].
    ///
    /// [`Scenario`]: gherkin::Scenario
    #[allow(clippy::missing_const_for_fn)] // false positive: drop in const
    #[must_use]
    pub fn retry_options<R>(mut self, func: R) -> Self
    where
        R: Fn(
                &gherkin::Feature,
                Option<&gherkin::Rule>,
                &gherkin::Scenario,
                &Cli,
            ) -> Option<RetryOptions>
            + 'static,
    {
        self.retry_options = Arc::new(func);
        self
    }

    /// Sets a hook, executed on each [`Scenario`] before running all its
    /// [`Step`]s, including [`Background`] ones.
    ///
    /// [`Background`]: gherkin::Background
    /// [`Scenario`]: gherkin::Scenario
    /// [`Step`]: gherkin::Step
    #[allow(clippy::missing_const_for_fn)] // false positive: drop in const
    #[must_use]
    pub fn before<Func>(self, func: Func) -> Basic<World, Which, Func, After>
    where
        Func: for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a mut World,
        ) -> LocalBoxFuture<'a, ()>,
    {
        let Self {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            which_scenario,
            retry_options,
            after_hook,
            fail_fast,
            ..
        } = self;
        Basic {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            which_scenario,
            retry_options,
            before_hook: Some(func),
            after_hook,
            fail_fast,
        }
    }

    /// Sets hook, executed on each [`Scenario`] after running all its
    /// [`Step`]s, even after [`Skipped`] of [`Failed`] ones.
    ///
    /// Last `World` argument is supplied to the function, in case it was
    /// initialized before by running [`before`] hook or any [`Step`].
    ///
    /// [`before`]: Self::before()
    /// [`Failed`]: event::Step::Failed
    /// [`Scenario`]: gherkin::Scenario
    /// [`Skipped`]: event::Step::Skipped
    /// [`Step`]: gherkin::Step
    #[allow(clippy::missing_const_for_fn)] // false positive: drop in const
    #[must_use]
    pub fn after<Func>(self, func: Func) -> Basic<World, Which, Before, Func>
    where
        Func: for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a event::ScenarioFinished,
            Option<&'a mut World>,
        ) -> LocalBoxFuture<'a, ()>,
    {
        let Self {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            which_scenario,
            retry_options,
            before_hook,
            fail_fast,
            ..
        } = self;
        Basic {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            which_scenario,
            retry_options,
            before_hook,
            after_hook: Some(func),
            fail_fast,
        }
    }

    /// Sets the given [`Collection`] of [`Step`]s to this [`Runner`].
    ///
    /// [`Collection`]: step::Collection
    #[allow(clippy::missing_const_for_fn)] // false positive: drop in const
    #[must_use]
    pub fn steps(mut self, steps: step::Collection<World>) -> Self {
        self.steps = steps;
        self
    }

    /// Adds a [Given] [`Step`] matching the given `regex`.
    ///
    /// [Given]: https://cucumber.io/docs/gherkin/reference#given
    #[must_use]
    pub fn given(mut self, regex: Regex, step: Step<World>) -> Self {
        self.steps = mem::take(&mut self.steps).given(None, regex, step);
        self
    }

    /// Adds a [When] [`Step`] matching the given `regex`.
    ///
    /// [When]: https://cucumber.io/docs/gherkin/reference#given
    #[must_use]
    pub fn when(mut self, regex: Regex, step: Step<World>) -> Self {
        self.steps = mem::take(&mut self.steps).when(None, regex, step);
        self
    }

    /// Adds a [Then] [`Step`] matching the given `regex`.
    ///
    /// [Then]: https://cucumber.io/docs/gherkin/reference#then
    #[must_use]
    pub fn then(mut self, regex: Regex, step: Step<World>) -> Self {
        self.steps = mem::take(&mut self.steps).then(None, regex, step);
        self
    }
}

impl<W, Which, Before, After> Runner<W> for Basic<W, Which, Before, After>
where
    W: World,
    Which: Fn(
            &gherkin::Feature,
            Option<&gherkin::Rule>,
            &gherkin::Scenario,
        ) -> ScenarioType
        + 'static,
    Before: for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a mut W,
        ) -> LocalBoxFuture<'a, ()>
        + 'static,
    After: for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a event::ScenarioFinished,
            Option<&'a mut W>,
        ) -> LocalBoxFuture<'a, ()>
        + 'static,
{
    type Cli = Cli;

    type EventStream =
        LocalBoxStream<'static, parser::Result<Event<event::Cucumber<W>>>>;

    fn run<S>(self, features: S, mut cli: Cli) -> Self::EventStream
    where
        S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
    {
        let Self {
            max_concurrent_scenarios,
            retries,
            retry_after,
            retry_filter,
            steps,
            which_scenario,
            retry_options,
            before_hook,
            after_hook,
            fail_fast,
        } = self;

        cli.retry = cli.retry.or(retries);
        cli.retry_after = cli.retry_after.or(retry_after);
        cli.retry_tag_filter = cli.retry_tag_filter.or(retry_filter);
        let fail_fast = cli.fail_fast || fail_fast;
        let concurrency = cli.concurrency.or(max_concurrent_scenarios);

        let buffer = Features::default();
        let (sender, receiver) = mpsc::unbounded();

        let insert = insert_features(
            buffer.clone(),
            features,
            which_scenario,
            retry_options,
            sender.clone(),
            cli,
            fail_fast,
        );
        let execute = execute(
            buffer,
            concurrency,
            steps,
            sender,
            before_hook,
            after_hook,
            fail_fast,
        );

        stream::select(
            receiver.map(Either::Left),
            future::join(insert, execute)
                .into_stream()
                .map(Either::Right),
        )
        .filter_map(|r| async {
            match r {
                Either::Left(ev) => Some(ev),
                Either::Right(_) => None,
            }
        })
        .boxed_local()
    }
}

/// Stores [`Feature`]s for later use by [`execute()`].
///
/// [`Feature`]: gherkin::Feature
async fn insert_features<W, S, F>(
    into: Features,
    features_stream: S,
    which_scenario: F,
    retries: RetryOptionsFn,
    sender: mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,
    cli: Cli,
    fail_fast: bool,
) where
    S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
    F: Fn(
            &gherkin::Feature,
            Option<&gherkin::Rule>,
            &gherkin::Scenario,
        ) -> ScenarioType
        + 'static,
{
    let mut features = 0;
    let mut rules = 0;
    let mut scenarios = 0;
    let mut steps = 0;
    let mut parser_errors = 0;

    pin_mut!(features_stream);
    while let Some(feat) = features_stream.next().await {
        match feat {
            Ok(f) => {
                features += 1;
                rules += f.rules.len();
                scenarios += f.count_scenarios();
                steps += f.count_steps();

                into.insert(f, &which_scenario, &retries, &cli).await;
            }
            Err(e) => {
                parser_errors += 1;

                // If the receiver end is dropped, then no one listens for the
                // events, so we can just stop from here.
                if sender.unbounded_send(Err(e)).is_err() || fail_fast {
                    break;
                }
            }
        }
    }

    drop(sender.unbounded_send(Ok(Event::new(
        event::Cucumber::ParsingFinished {
            features,
            rules,
            scenarios,
            steps,
            parser_errors,
        },
    ))));

    into.finish();
}

/// Retrieves [`Feature`]s and executes them.
///
/// # Events
///
/// - [`Scenario`] events are emitted by [`Executor`].
/// - If [`Scenario`] was first or last for particular [`Rule`] or [`Feature`],
///   emits starting or finishing events for them.
///
/// [`Feature`]: gherkin::Feature
/// [`Rule`]: gherkin::Rule
/// [`Scenario`]: gherkin::Scenario
async fn execute<W, Before, After>(
    features: Features,
    max_concurrent_scenarios: Option<usize>,
    collection: step::Collection<W>,
    event_sender: mpsc::UnboundedSender<
        parser::Result<Event<event::Cucumber<W>>>,
    >,
    before_hook: Option<Before>,
    after_hook: Option<After>,
    fail_fast: bool,
) where
    W: World,
    Before: 'static
        + for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a mut W,
        ) -> LocalBoxFuture<'a, ()>,
    After: 'static
        + for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a event::ScenarioFinished,
            Option<&'a mut W>,
        ) -> LocalBoxFuture<'a, ()>,
{
    // Those panic hook shenanigans are done to avoid console messages like
    // "thread 'main' panicked at ..."
    //
    // 1. We obtain the current panic hook and replace it with an empty one.
    // 2. We run tests, which can panic. In that case we pass all panic info
    //    down the line to the Writer, which will print it at a right time.
    // 3. We restore original panic hook, because suppressing all panics doesn't
    //    sound like a very good idea.
    let hook = panic::take_hook();
    panic::set_hook(Box::new(|_| {}));

    let (finished_sender, finished_receiver) = mpsc::unbounded();
    let mut storage = FinishedRulesAndFeatures::new(finished_receiver);
    let executor = Executor::new(
        collection,
        before_hook,
        after_hook,
        event_sender,
        finished_sender,
        features.clone(),
    );

    executor.send_event(event::Cucumber::Started);

    // TODO: Replace with `ControlFlow::map_break()` once stabilized:
    //       https://github.com/rust-lang/rust/issues/75744
    let map_break = |cf| match cf {
        ControlFlow::Continue(cont) => cont,
        ControlFlow::Break(()) => Some(0),
    };

    let mut started_scenarios = ControlFlow::Continue(max_concurrent_scenarios);
    let mut run_scenarios = stream::FuturesUnordered::new();
    loop {
        // We yield once on every iteration, because there is a chance, that
        // this function never yields otherwise. In this case event sender won't
        // send anything to the `Writer` until the end. This is the case, when
        // all the parsing is done, so there is no contention on the `Mutex`
        // inside `Features` storage and all `Step` functions don't yield.
        yield_now().await;

        let (runnable, sleep) =
            features.get(map_break(started_scenarios)).await;
        if run_scenarios.is_empty() && runnable.is_empty() {
            if features.is_finished(started_scenarios.is_break()).await {
                break;
            }

            // To avoid busy-polling of `Features::get()`, in case there are no
            // scenarios that are running or scheduled for execution, we spawn a
            // thread, that sleeps for minimal deadline of all retried
            // scenarios.
            // TODO: Replace `thread::spawn` with async runtime agnostic sleep,
            //       once it's available.
            if let Some(dur) = sleep {
                let (sender, receiver) = oneshot::channel();
                drop(thread::spawn(move || {
                    thread::sleep(dur);
                    sender.send(())
                }));
                let _ = receiver.await.ok();
            }

            continue;
        }

        let started = storage.start_scenarios(&runnable);
        executor.send_all_events(started);

        if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios {
            *sc -= runnable.len();
        }

        for (f, r, s, ty, retries) in runnable {
            run_scenarios.push(executor.run_scenario(f, r, s, ty, retries));
        }

        if run_scenarios.next().await.is_some() {
            if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios {
                *sc += 1;
            }
        }

        while let Ok(Some((feat, rule, scenario_failed, retried))) =
            storage.finished_receiver.try_next()
        {
            if let Some(rule) = rule {
                if let Some(f) = storage.rule_scenario_finished(
                    Arc::clone(&feat),
                    rule,
                    retried,
                ) {
                    executor.send_event(f);
                }
            }
            if let Some(f) = storage.feature_scenario_finished(feat, retried) {
                executor.send_event(f);
            }

            if fail_fast && scenario_failed && !retried {
                started_scenarios = ControlFlow::Break(());
            }
        }
    }

    // This is done in case of `fail_fast: true`, when not all `Scenario`s might
    // be executed.
    executor.send_all_events(storage.finish_all_rules_and_features());

    executor.send_event(event::Cucumber::Finished);

    panic::set_hook(hook);
}

/// Runs [`Scenario`]s and notifies about their state of completion.
///
/// [`Scenario`]: gherkin::Scenario
struct Executor<W, Before, After> {
    /// [`Step`]s [`Collection`].
    ///
    /// [`Collection`]: step::Collection
    collection: step::Collection<W>,

    /// Function, executed on each [`Scenario`] before running all [`Step`]s,
    /// including [`Background`] ones.
    ///
    /// [`Background`]: gherkin::Background
    /// [`Scenario`]: gherkin::Scenario
    /// [`Step`]: gherkin::Step
    before_hook: Option<Before>,

    /// Function, executed on each [`Scenario`] after running all [`Step`]s.
    ///
    /// [`Scenario`]: gherkin::Scenario
    /// [`Step`]: gherkin::Step
    after_hook: Option<After>,

    /// Sender for [`Scenario`] [events][1].
    ///
    /// [`Scenario`]: gherkin::Scenario
    /// [1]: event::Scenario
    event_sender:
        mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,

    /// Sender for notifying of [`Scenario`]s completion.
    ///
    /// [`Scenario`]: gherkin::Scenario
    finished_sender: FinishedFeaturesSender,

    /// [`Scenario`]s storage.
    ///
    /// [`Scenario`]: gherkin::Scenario
    storage: Features,
}

impl<W: World, Before, After> Executor<W, Before, After>
where
    Before: 'static
        + for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a mut W,
        ) -> LocalBoxFuture<'a, ()>,
    After: 'static
        + for<'a> Fn(
            &'a gherkin::Feature,
            Option<&'a gherkin::Rule>,
            &'a gherkin::Scenario,
            &'a event::ScenarioFinished,
            Option<&'a mut W>,
        ) -> LocalBoxFuture<'a, ()>,
{
    /// Creates a new [`Executor`].
    const fn new(
        collection: step::Collection<W>,
        before_hook: Option<Before>,
        after_hook: Option<After>,
        event_sender: mpsc::UnboundedSender<
            parser::Result<Event<event::Cucumber<W>>>,
        >,
        finished_sender: FinishedFeaturesSender,
        storage: Features,
    ) -> Self {
        Self {
            collection,
            before_hook,
            after_hook,
            event_sender,
            finished_sender,
            storage,
        }
    }

    /// Runs a [`Scenario`].
    ///
    /// # Events
    ///
    /// - Emits all [`Scenario`] events.
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Rule`]: gherkin::Rule
    /// [`Scenario`]: gherkin::Scenario
    #[allow(clippy::too_many_lines)]
    async fn run_scenario(
        &self,
        feature: Arc<gherkin::Feature>,
        rule: Option<Arc<gherkin::Rule>>,
        scenario: Arc<gherkin::Scenario>,
        scenario_ty: ScenarioType,
        retries: Option<RetryOptions>,
    ) {
        let retry_num = retries.map(|r| r.retries);
        let ok = |e: fn(_) -> event::Scenario<W>| {
            let (f, r, s) = (&feature, &rule, &scenario);
            move |step| {
                let (f, r, s) = (Arc::clone(f), r.clone(), Arc::clone(s));
                let event = e(step).with_retries(retry_num);
                event::Cucumber::scenario(f, r, s, event)
            }
        };
        let ok_capt = |e: fn(_, _, _) -> event::Scenario<W>| {
            let (f, r, s) = (&feature, &rule, &scenario);
            move |step, cap, loc| {
                let (f, r, s) = (Arc::clone(f), r.clone(), Arc::clone(s));
                let event = e(step, cap, loc).with_retries(retry_num);
                event::Cucumber::scenario(f, r, s, event)
            }
        };

        let compose = |started, passed, skipped| {
            (ok(started), ok_capt(passed), ok(skipped))
        };
        let into_bg_step_ev = compose(
            event::Scenario::background_step_started,
            event::Scenario::background_step_passed,
            event::Scenario::background_step_skipped,
        );
        let into_step_ev = compose(
            event::Scenario::step_started,
            event::Scenario::step_passed,
            event::Scenario::step_skipped,
        );

        self.send_event(event::Cucumber::scenario(
            Arc::clone(&feature),
            rule.clone(),
            Arc::clone(&scenario),
            event::Scenario::Started.with_retries(retry_num),
        ));

        let mut result = async {
            let before_hook = self
                .run_before_hook(&feature, rule.as_ref(), &scenario, retry_num)
                .await?;

            let feature_background = feature
                .background
                .as_ref()
                .map(|b| b.steps.iter().map(|s| Arc::new(s.clone())))
                .into_iter()
                .flatten();

            let feature_background = stream::iter(feature_background)
                .map(Ok)
                .try_fold(before_hook, |world, bg_step| {
                    self.run_step(world, bg_step, true, into_bg_step_ev)
                        .map_ok(Some)
                })
                .await?;

            let rule_background = rule
                .as_ref()
                .map(|r| {
                    r.background
                        .as_ref()
                        .map(|b| b.steps.iter().map(|s| Arc::new(s.clone())))
                        .into_iter()
                        .flatten()
                })
                .into_iter()
                .flatten();

            let rule_background = stream::iter(rule_background)
                .map(Ok)
                .try_fold(feature_background, |world, bg_step| {
                    self.run_step(world, bg_step, true, into_bg_step_ev)
                        .map_ok(Some)
                })
                .await?;

            stream::iter(scenario.steps.iter().map(|s| Arc::new(s.clone())))
                .map(Ok)
                .try_fold(rule_background, |world, step| {
                    self.run_step(world, step, false, into_step_ev).map_ok(Some)
                })
                .await
        }
        .await;

        let (world, scenario_finished_ev) = match &mut result {
            Ok(world) => (world.take(), event::ScenarioFinished::StepPassed),
            Err(exec_err) => (
                exec_err.take_world(),
                exec_err.get_scenario_finished_event(),
            ),
        };

        let (world, after_hook_meta, after_hook_error) = self
            .run_after_hook(
                world,
                &feature,
                rule.as_ref(),
                &scenario,
                scenario_finished_ev,
            )
            .await
            .map_or_else(
                |(w, meta, info)| (w.map(Arc::new), Some(meta), Some(info)),
                |(w, meta)| (w.map(Arc::new), meta, None),
            );

        let scenario_failed = match &result {
            Ok(_) | Err(ExecutionFailure::StepSkipped(_)) => false,
            Err(
                ExecutionFailure::BeforeHookPanicked { .. }
                | ExecutionFailure::StepPanicked { .. },
            ) => true,
        };
        let is_failed = scenario_failed || after_hook_error.is_some();

        if let Some(exec_error) = result.err() {
            self.emit_failed_events(
                Arc::clone(&feature),
                rule.clone(),
                Arc::clone(&scenario),
                world.clone(),
                exec_error,
                retry_num,
            );
        }

        self.emit_after_hook_events(
            Arc::clone(&feature),
            rule.clone(),
            Arc::clone(&scenario),
            world,
            after_hook_meta,
            after_hook_error,
            retry_num,
        );

        self.send_event(event::Cucumber::scenario(
            Arc::clone(&feature),
            rule.clone(),
            Arc::clone(&scenario),
            event::Scenario::Finished.with_retries(retry_num),
        ));

        let next_try = retries
            .filter(|_| is_failed)
            .and_then(RetryOptions::next_try);
        if let Some(next_try) = next_try {
            self.storage
                .insert_retried_scenario(
                    Arc::clone(&feature),
                    rule.clone(),
                    scenario,
                    scenario_ty,
                    Some(next_try),
                )
                .await;
        }

        self.scenario_finished(feature, rule, is_failed, next_try.is_some());
    }

    /// Executes [`HookType::Before`], if present.
    ///
    /// # Events
    ///
    /// - Emits all the [`HookType::Before`] events, except [`Hook::Failed`].
    ///   See [`Self::emit_failed_events()`] for more details.
    ///
    /// [`Hook::Failed`]: event::Hook::Failed
    async fn run_before_hook(
        &self,
        feature: &Arc<gherkin::Feature>,
        rule: Option<&Arc<gherkin::Rule>>,
        scenario: &Arc<gherkin::Scenario>,
        retries: Option<Retries>,
    ) -> Result<Option<W>, ExecutionFailure<W>> {
        let init_world = async {
            AssertUnwindSafe(async { W::new().await })
                .catch_unwind()
                .await
                .map_err(Info::from)
                .and_then(|r| {
                    r.map_err(|e| {
                        coerce_into_info(format!(
                            "failed to initialize World: {e}",
                        ))
                    })
                })
                .map_err(|info| (info, None))
        };

        if let Some(hook) = self.before_hook.as_ref() {
            self.send_event(event::Cucumber::scenario(
                Arc::clone(feature),
                rule.map(Arc::clone),
                Arc::clone(scenario),
                event::Scenario::hook_started(HookType::Before)
                    .with_retries(retries),
            ));

            let fut = init_world.and_then(|mut world| async {
                let fut = async {
                    (hook)(
                        feature.as_ref(),
                        rule.as_ref().map(AsRef::as_ref),
                        scenario.as_ref(),
                        &mut world,
                    )
                    .await;
                };
                match AssertUnwindSafe(fut).catch_unwind().await {
                    Ok(()) => Ok(world),
                    Err(i) => Err((Info::from(i), Some(world))),
                }
            });

            match fut.await {
                Ok(world) => {
                    self.send_event(event::Cucumber::scenario(
                        Arc::clone(feature),
                        rule.map(Arc::clone),
                        Arc::clone(scenario),
                        event::Scenario::hook_passed(HookType::Before)
                            .with_retries(retries),
                    ));
                    Ok(Some(world))
                }
                Err((panic_info, world)) => {
                    Err(ExecutionFailure::BeforeHookPanicked {
                        world,
                        panic_info,
                        meta: event::Metadata::new(()),
                    })
                }
            }
        } else {
            Ok(None)
        }
    }

    /// Runs a [`Step`].
    ///
    /// # Events
    ///
    /// - Emits all the [`Step`] events, except [`Step::Failed`]. See
    ///   [`Self::emit_failed_events()`] for more details.
    ///
    /// [`Step`]: gherkin::Step
    /// [`Step::Failed`]: event::Step::Failed
    async fn run_step<St, Ps, Sk>(
        &self,
        world_opt: Option<W>,
        step: Arc<gherkin::Step>,
        is_background: bool,
        (started, passed, skipped): (St, Ps, Sk),
    ) -> Result<W, ExecutionFailure<W>>
    where
        St: FnOnce(Arc<gherkin::Step>) -> event::Cucumber<W>,
        Ps: FnOnce(
            Arc<gherkin::Step>,
            CaptureLocations,
            Option<step::Location>,
        ) -> event::Cucumber<W>,
        Sk: FnOnce(Arc<gherkin::Step>) -> event::Cucumber<W>,
    {
        self.send_event(started(Arc::clone(&step)));

        let run = async {
            let (step_fn, captures, loc, ctx) =
                match self.collection.find(&step) {
                    Ok(Some(f)) => f,
                    Ok(None) => return Ok((None, None, world_opt)),
                    Err(e) => {
                        let e = event::StepError::AmbiguousMatch(e);
                        return Err((e, None, None, world_opt));
                    }
                };

            let mut world = if let Some(w) = world_opt {
                w
            } else {
                match AssertUnwindSafe(async { W::new().await })
                    .catch_unwind()
                    .await
                {
                    Ok(Ok(w)) => w,
                    Ok(Err(e)) => {
                        let e = event::StepError::Panic(coerce_into_info(
                            format!("failed to initialize `World`: {e}"),
                        ));
                        return Err((e, None, loc, None));
                    }
                    Err(e) => {
                        let e = event::StepError::Panic(e.into());
                        return Err((e, None, loc, None));
                    }
                }
            };

            match AssertUnwindSafe(async { step_fn(&mut world, ctx).await })
                .catch_unwind()
                .await
            {
                Ok(()) => Ok((Some(captures), loc, Some(world))),
                Err(e) => {
                    let e = event::StepError::Panic(e.into());
                    Err((e, Some(captures), loc, Some(world)))
                }
            }
        };

        match run.await {
            Ok((Some(captures), loc, Some(world))) => {
                self.send_event(passed(step, captures, loc));
                Ok(world)
            }
            Ok((_, _, world)) => {
                self.send_event(skipped(step));
                Err(ExecutionFailure::StepSkipped(world))
            }
            Err((err, captures, loc, world)) => {
                Err(ExecutionFailure::StepPanicked {
                    world,
                    step,
                    captures,
                    loc,
                    err,
                    meta: event::Metadata::new(()),
                    is_background,
                })
            }
        }
    }

    /// Emits all the failure events of [`HookType::Before`] or [`Step`] after
    /// executing the [`Self::run_after_hook()`].
    ///
    /// This is done because [`HookType::After`] requires a mutable reference to
    /// the [`World`] while on the other hand we store immutable reference to it
    /// inside failure events for easier debugging. So, to avoid imposing
    /// additional [`Clone`] bounds on the [`World`], we run the
    /// [`HookType::After`] first without emitting any events about its
    /// execution, then emit failure event of the [`HookType::Before`] or
    /// [`Step`], if present, and finally emit all the [`HookType::After`]
    /// events. This allows us to ensure [order guarantees][1] while not
    /// restricting the [`HookType::After`] to the immutable reference. The only
    /// downside of this approach is that we may emit failure events of
    /// [`HookType::Before`] or [`Step`] with the [`World`] state being changed
    /// by the [`HookType::After`].
    ///
    /// [`Step`]: gherkin::Step
    /// [1]: crate::Runner#order-guarantees
    fn emit_failed_events(
        &self,
        feature: Arc<gherkin::Feature>,
        rule: Option<Arc<gherkin::Rule>>,
        scenario: Arc<gherkin::Scenario>,
        world: Option<Arc<W>>,
        err: ExecutionFailure<W>,
        retries: Option<Retries>,
    ) {
        match err {
            ExecutionFailure::StepSkipped(_) => {}
            ExecutionFailure::BeforeHookPanicked {
                panic_info, meta, ..
            } => {
                self.send_event_with_meta(
                    event::Cucumber::scenario(
                        feature,
                        rule,
                        scenario,
                        event::Scenario::hook_failed(
                            HookType::Before,
                            world,
                            panic_info,
                        )
                        .with_retries(retries),
                    ),
                    meta,
                );
            }
            ExecutionFailure::StepPanicked {
                step,
                captures,
                loc,
                err: error,
                meta,
                is_background: true,
                ..
            } => self.send_event_with_meta(
                event::Cucumber::scenario(
                    feature,
                    rule,
                    scenario,
                    event::Scenario::background_step_failed(
                        step, captures, loc, world, error,
                    )
                    .with_retries(retries),
                ),
                meta,
            ),
            ExecutionFailure::StepPanicked {
                step,
                captures,
                loc,
                err: error,
                meta,
                is_background: false,
                ..
            } => self.send_event_with_meta(
                event::Cucumber::scenario(
                    feature,
                    rule,
                    scenario,
                    event::Scenario::step_failed(
                        step, captures, loc, world, error,
                    )
                    .with_retries(retries),
                ),
                meta,
            ),
        }
    }

    /// Executes the [`HookType::After`], if present.
    ///
    /// Doesn't emit any events, see [`Self::emit_failed_events()`] for more
    /// details.
    async fn run_after_hook(
        &self,
        mut world: Option<W>,
        feature: &Arc<gherkin::Feature>,
        rule: Option<&Arc<gherkin::Rule>>,
        scenario: &Arc<gherkin::Scenario>,
        ev: event::ScenarioFinished,
    ) -> Result<
        (Option<W>, Option<AfterHookEventsMeta>),
        (Option<W>, AfterHookEventsMeta, Info),
    > {
        if let Some(hook) = self.after_hook.as_ref() {
            let fut = async {
                (hook)(
                    feature.as_ref(),
                    rule.as_ref().map(AsRef::as_ref),
                    scenario.as_ref(),
                    &ev,
                    world.as_mut(),
                )
                .await;
            };

            let started = event::Metadata::new(());
            let res = AssertUnwindSafe(fut).catch_unwind().await;
            let finished = event::Metadata::new(());
            let meta = AfterHookEventsMeta { started, finished };

            match res {
                Ok(()) => Ok((world, Some(meta))),
                Err(info) => Err((world, meta, info.into())),
            }
        } else {
            Ok((world, None))
        }
    }

    /// Emits all the [`HookType::After`] events.
    ///
    /// See [`Self::emit_failed_events()`] for the explanation why we don't do
    /// that inside [`Self::run_after_hook()`].
    #[allow(clippy::too_many_arguments)]
    fn emit_after_hook_events(
        &self,
        feature: Arc<gherkin::Feature>,
        rule: Option<Arc<gherkin::Rule>>,
        scenario: Arc<gherkin::Scenario>,
        world: Option<Arc<W>>,
        meta: Option<AfterHookEventsMeta>,
        err: Option<Info>,
        retries: Option<Retries>,
    ) {
        debug_assert_eq!(self.after_hook.is_some(), meta.is_some());

        if let Some(meta) = meta {
            self.send_event_with_meta(
                event::Cucumber::scenario(
                    Arc::clone(&feature),
                    rule.clone(),
                    Arc::clone(&scenario),
                    event::Scenario::hook_started(HookType::After)
                        .with_retries(retries),
                ),
                meta.started,
            );

            let ev = if let Some(err) = err {
                event::Cucumber::scenario(
                    feature,
                    rule,
                    scenario,
                    event::Scenario::hook_failed(HookType::After, world, err)
                        .with_retries(retries),
                )
            } else {
                event::Cucumber::scenario(
                    feature,
                    rule,
                    scenario,
                    event::Scenario::hook_passed(HookType::After)
                        .with_retries(retries),
                )
            };

            self.send_event_with_meta(ev, meta.finished);
        }
    }

    /// Notifies [`FinishedRulesAndFeatures`] about [`Scenario`] being finished.
    ///
    /// [`Scenario`]: gherkin::Scenario
    fn scenario_finished(
        &self,
        feature: Arc<gherkin::Feature>,
        rule: Option<Arc<gherkin::Rule>>,
        is_failed: IsFailed,
        is_retried: IsRetried,
    ) {
        // If the receiver end is dropped, then no one listens for events
        // so we can just ignore it.
        drop(
            self.finished_sender
                .unbounded_send((feature, rule, is_failed, is_retried)),
        );
    }

    /// Notifies with the given [`Cucumber`] event.
    ///
    /// [`Cucumber`]: event::Cucumber
    fn send_event(&self, event: event::Cucumber<W>) {
        // If the receiver end is dropped, then no one listens for events,
        // so we can just ignore it.
        drop(self.event_sender.unbounded_send(Ok(Event::new(event))));
    }

    /// Notifies with the given [`Cucumber`] event along with its [`Metadata`].
    ///
    /// [`Cucumber`]: event::Cucumber
    /// [`Metadata`]: event::Metadata
    fn send_event_with_meta(
        &self,
        event: event::Cucumber<W>,
        meta: event::Metadata,
    ) {
        // If the receiver end is dropped, then no one listens for events,
        // so we can just ignore it.
        drop(self.event_sender.unbounded_send(Ok(meta.wrap(event))));
    }

    /// Notifies with the given [`Cucumber`] events.
    ///
    /// [`Cucumber`]: event::Cucumber
    fn send_all_events(
        &self,
        events: impl Iterator<Item = event::Cucumber<W>>,
    ) {
        for v in events {
            // If the receiver end is dropped, then no one listens for events,
            // so we can just stop from here.
            if self.event_sender.unbounded_send(Ok(Event::new(v))).is_err() {
                break;
            }
        }
    }
}

/// Stores currently running [`Rule`]s and [`Feature`]s and notifies about their
/// state of completion.
///
/// [`Feature`]: gherkin::Feature
/// [`Rule`]: gherkin::Rule
struct FinishedRulesAndFeatures {
    /// Number of finished [`Scenario`]s of [`Feature`].
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Scenario`]: gherkin::Scenario
    features_scenarios_count: HashMap<Arc<gherkin::Feature>, usize>,

    /// Number of finished [`Scenario`]s of [`Rule`].
    ///
    /// We also store path to a [`Feature`], so [`Rule`]s with same names and
    /// spans in different `.feature` files will have different hashes.
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Rule`]: gherkin::Rule
    /// [`Scenario`]: gherkin::Scenario
    rule_scenarios_count:
        HashMap<(Arc<gherkin::Feature>, Arc<gherkin::Rule>), usize>,

    /// Receiver for notifying state of [`Scenario`]s completion.
    ///
    /// [`Scenario`]: gherkin::Scenario
    finished_receiver: FinishedFeaturesReceiver,
}

/// Alias of a [`mpsc::UnboundedSender`] that notifies about finished
/// [`Feature`]s.
///
/// [`Feature`]: gherkin::Feature
type FinishedFeaturesSender = mpsc::UnboundedSender<(
    Arc<gherkin::Feature>,
    Option<Arc<gherkin::Rule>>,
    IsFailed,
    IsRetried,
)>;

/// Alias of a [`mpsc::UnboundedReceiver`] that receives events about finished
/// [`Feature`]s.
///
/// [`Feature`]: gherkin::Feature
type FinishedFeaturesReceiver = mpsc::UnboundedReceiver<(
    Arc<gherkin::Feature>,
    Option<Arc<gherkin::Rule>>,
    IsFailed,
    IsRetried,
)>;

impl FinishedRulesAndFeatures {
    /// Creates a new [`FinishedRulesAndFeatures`] store.
    fn new(finished_receiver: FinishedFeaturesReceiver) -> Self {
        Self {
            features_scenarios_count: HashMap::new(),
            rule_scenarios_count: HashMap::new(),
            finished_receiver,
        }
    }

    /// Marks [`Rule`]'s [`Scenario`] as finished and returns [`Rule::Finished`]
    /// event if no [`Scenario`]s left.
    ///
    /// [`Rule`]: gherkin::Rule
    /// [`Rule::Finished`]: event::Rule::Finished
    /// [`Scenario`]: gherkin::Scenario
    fn rule_scenario_finished<W>(
        &mut self,
        feature: Arc<gherkin::Feature>,
        rule: Arc<gherkin::Rule>,
        is_retried: bool,
    ) -> Option<event::Cucumber<W>> {
        if is_retried {
            return None;
        }

        let finished_scenarios = self
            .rule_scenarios_count
            .get_mut(&(Arc::clone(&feature), Arc::clone(&rule)))
            .unwrap_or_else(|| panic!("No Rule {}", rule.name));
        *finished_scenarios += 1;
        (rule.scenarios.len() == *finished_scenarios).then(|| {
            let _ = self
                .rule_scenarios_count
                .remove(&(Arc::clone(&feature), Arc::clone(&rule)));
            event::Cucumber::rule_finished(feature, rule)
        })
    }

    /// Marks [`Feature`]'s [`Scenario`] as finished and returns
    /// [`Feature::Finished`] event if no [`Scenario`]s left.
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Feature::Finished`]: event::Feature::Finished
    /// [`Scenario`]: gherkin::Scenario
    fn feature_scenario_finished<W>(
        &mut self,
        feature: Arc<gherkin::Feature>,
        is_retried: bool,
    ) -> Option<event::Cucumber<W>> {
        if is_retried {
            return None;
        }

        let finished_scenarios = self
            .features_scenarios_count
            .get_mut(&feature)
            .unwrap_or_else(|| panic!("No Feature {}", feature.name));
        *finished_scenarios += 1;
        let scenarios = feature.count_scenarios();
        (scenarios == *finished_scenarios).then(|| {
            let _ = self.features_scenarios_count.remove(&feature);
            event::Cucumber::feature_finished(feature)
        })
    }

    /// Marks all the unfinished [`Rule`]s and [`Feature`]s as finished, and
    /// returns all the appropriate finished events.
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Rule`]: gherkin::Rule
    fn finish_all_rules_and_features<W>(
        &mut self,
    ) -> impl Iterator<Item = event::Cucumber<W>> + '_ {
        self.rule_scenarios_count
            .drain()
            .map(|((feat, rule), _)| event::Cucumber::rule_finished(feat, rule))
            .chain(
                self.features_scenarios_count
                    .drain()
                    .map(|(feat, _)| event::Cucumber::feature_finished(feat)),
            )
    }

    /// Marks [`Scenario`]s as started and returns [`Rule::Started`] and
    /// [`Feature::Started`] if given [`Scenario`] was first for particular
    /// [`Rule`] or [`Feature`].
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Feature::Started`]: event::Feature::Started
    /// [`Rule`]: gherkin::Rule
    /// [`Rule::Started`]: event::Rule::Started
    /// [`Scenario`]: gherkin::Scenario
    fn start_scenarios<W>(
        &mut self,
        runnable: impl AsRef<
            [(
                Arc<gherkin::Feature>,
                Option<Arc<gherkin::Rule>>,
                Arc<gherkin::Scenario>,
                ScenarioType,
                Option<RetryOptions>,
            )],
        >,
    ) -> impl Iterator<Item = event::Cucumber<W>> {
        let runnable = runnable.as_ref();

        let mut started_features = Vec::new();
        for feature in runnable.iter().map(|(f, ..)| Arc::clone(f)).dedup() {
            let _ = self
                .features_scenarios_count
                .entry(Arc::clone(&feature))
                .or_insert_with(|| {
                    started_features.push(feature);
                    0
                });
        }

        let mut started_rules = Vec::new();
        for (feat, rule) in runnable
            .iter()
            .filter_map(|(feat, rule, _, _, _)| {
                rule.clone().map(|r| (Arc::clone(feat), r))
            })
            .dedup()
        {
            let _ = self
                .rule_scenarios_count
                .entry((Arc::clone(&feat), Arc::clone(&rule)))
                .or_insert_with(|| {
                    started_rules.push((feat, rule));
                    0
                });
        }

        started_features
            .into_iter()
            .map(event::Cucumber::feature_started)
            .chain(
                started_rules
                    .into_iter()
                    .map(|(f, r)| event::Cucumber::rule_started(f, r)),
            )
    }
}

/// [`Scenario`]s storage.
///
/// [`Scenario`]: gherkin::Scenario
type Scenarios = HashMap<
    ScenarioType,
    Vec<(
        Arc<gherkin::Feature>,
        Option<Arc<gherkin::Rule>>,
        Arc<gherkin::Scenario>,
        Option<RetryOptionsWithDeadline>,
    )>,
>;

/// Alias of a [`Features::insert_scenarios()`] argument.
type InsertedScenarios = HashMap<
    ScenarioType,
    Vec<(
        Arc<gherkin::Feature>,
        Option<Arc<gherkin::Rule>>,
        Arc<gherkin::Scenario>,
        Option<RetryOptions>,
    )>,
>;

/// Storage sorted by [`ScenarioType`] [`Feature`]'s [`Scenario`]s.
///
/// [`Feature`]: gherkin::Feature
/// [`Scenario`]: gherkin::Scenario
#[derive(Clone, Default)]
struct Features {
    /// Storage itself.
    scenarios: Arc<Mutex<Scenarios>>,

    /// Indicates whether all parsed [`Feature`]s are sorted and stored.
    ///
    /// [`Feature`]: gherkin::Feature
    finished: Arc<AtomicBool>,
}

impl Features {
    /// Splits [`Feature`] into [`Scenario`]s, sorts by [`ScenarioType`] and
    /// stores them.
    ///
    /// [`Feature`]: gherkin::Feature
    /// [`Scenario`]: gherkin::Scenario
    async fn insert<Which>(
        &self,
        feature: gherkin::Feature,
        which_scenario: &Which,
        retry: &RetryOptionsFn,
        cli: &Cli,
    ) where
        Which: Fn(
                &gherkin::Feature,
                Option<&gherkin::Rule>,
                &gherkin::Scenario,
            ) -> ScenarioType
            + 'static,
    {
        let local = feature
            .scenarios
            .iter()
            .map(|s| (&feature, None, s))
            .chain(feature.rules.iter().flat_map(|r| {
                r.scenarios
                    .iter()
                    .map(|s| (&feature, Some(r), s))
                    .collect::<Vec<_>>()
            }))
            .map(|(feat, rule, scenario)| {
                let retries = retry(feat, rule, scenario, cli);
                (
                    Arc::new(feat.clone()),
                    rule.map(|r| Arc::new(r.clone())),
                    Arc::new(scenario.clone()),
                    retries,
                )
            })
            .into_group_map_by(|(f, r, s, _)| {
                which_scenario(f, r.as_ref().map(AsRef::as_ref), s)
            });

        self.insert_scenarios(local).await;
    }

    /// Inserts the provided retried [`Scenario`] into this [`Features`]
    /// storage.
    ///
    /// [`Scenario`]: gherkin::Scenario
    async fn insert_retried_scenario(
        &self,
        feature: Arc<gherkin::Feature>,
        rule: Option<Arc<gherkin::Rule>>,
        scenario: Arc<gherkin::Scenario>,
        scenario_ty: ScenarioType,
        retries: Option<RetryOptions>,
    ) {
        self.insert_scenarios(
            iter::once((scenario_ty, vec![(feature, rule, scenario, retries)]))
                .collect(),
        )
        .await;
    }

    /// Inserts the provided [`Scenario`]s into this [`Features`] storage.
    ///
    /// [`Scenario`]: gherkin::Scenario
    async fn insert_scenarios(&self, scenarios: InsertedScenarios) {
        let now = Instant::now();

        let mut with_retries = HashMap::<_, Vec<_>>::new();
        let mut without_retries: Scenarios = HashMap::new();
        for (which, values) in scenarios {
            for (f, r, s, ret) in values {
                match ret {
                    ret @ (None
                    | Some(RetryOptions {
                        retries: Retries { current: 0, .. },
                        ..
                    })) => {
                        // `Retries::current` is `0`, so this `Scenario` run is
                        // initial and we don't need to wait for retry delay.
                        let ret = ret.map(RetryOptions::without_deadline);
                        without_retries
                            .entry(which)
                            .or_default()
                            .push((f, r, s, ret));
                    }
                    Some(ret) => {
                        let ret = ret.with_deadline(now);
                        with_retries
                            .entry(which)
                            .or_default()
                            .push((f, r, s, ret));
                    }
                }
            }
        }

        let mut storage = self.scenarios.lock().await;

        for (which, values) in with_retries {
            let ty_storage = storage.entry(which).or_default();
            for (f, r, s, ret) in values {
                ty_storage.insert(0, (f, r, s, Some(ret)));
            }
        }

        if without_retries.get(&ScenarioType::Serial).is_none() {
            // If there are no Serial Scenarios we just extending already
            // existing Concurrent Scenarios.
            for (which, values) in without_retries {
                storage.entry(which).or_default().extend(values);
            }
        } else {
            // If there are Serial Scenarios we insert all Serial and Concurrent
            // Scenarios in front.
            // This is done to execute them closely to one another, so the
            // output wouldn't hang on executing other Concurrent Scenarios.
            for (which, mut values) in without_retries {
                let old = mem::take(storage.entry(which).or_default());
                values.extend(old);
                storage.entry(which).or_default().extend(values);
            }
        }
    }

    /// Returns [`Scenario`]s which are ready to run and the minimal deadline of
    /// all retried [`Scenario`]s.
    ///
    /// [`Scenario`]: gherkin::Scenario
    async fn get(
        &self,
        max_concurrent_scenarios: Option<usize>,
    ) -> (
        Vec<(
            Arc<gherkin::Feature>,
            Option<Arc<gherkin::Rule>>,
            Arc<gherkin::Scenario>,
            ScenarioType,
            Option<RetryOptions>,
        )>,
        Option<Duration>,
    ) {
        use ScenarioType::{Concurrent, Serial};

        if max_concurrent_scenarios == Some(0) {
            return (Vec::new(), None);
        }

        let mut min_dur = None;
        let mut drain =
            |storage: &mut Vec<(_, _, _, Option<RetryOptionsWithDeadline>)>,
             ty,
             count: Option<usize>| {
                let mut i = 0;
                // TODO: Replace with `drain_filter`, once stabilized.
                //       https://github.com/rust-lang/rust/issues/43244
                let drained =
                    VecExt::drain_filter(storage, |(_, _, _, ret)| {
                        // Because `drain_filter` runs over entire `Vec` on
                        // `Drop`, we can't just `.take(count)`.
                        if count.filter(|c| i >= *c).is_some() {
                            return false;
                        }

                        ret.as_ref()
                            .and_then(
                                RetryOptionsWithDeadline::left_until_retry,
                            )
                            .map_or_else(
                                || {
                                    i += 1;
                                    true
                                },
                                |left| {
                                    min_dur = min_dur
                                        .map(|min| cmp::min(min, left))
                                        .or(Some(left));
                                    false
                                },
                            )
                    })
                    .map(|(f, r, s, ret)| (f, r, s, ty, ret.map(Into::into)))
                    .collect::<Vec<_>>();
                (!drained.is_empty()).then_some(drained)
            };

        let mut guard = self.scenarios.lock().await;
        let scenarios = guard
            .get_mut(&Serial)
            .and_then(|storage| drain(storage, Serial, Some(1)))
            .or_else(|| {
                guard.get_mut(&Concurrent).and_then(|storage| {
                    drain(storage, Concurrent, max_concurrent_scenarios)
                })
            })
            .unwrap_or_default();

        (scenarios, min_dur)
    }

    /// Marks that there will be no more [`Feature`]s to execute.
    ///
    /// [`Feature`]: gherkin::Feature
    fn finish(&self) {
        self.finished.store(true, Ordering::SeqCst);
    }

    /// Indicates whether there are more [`Feature`]s to execute.
    ///
    /// `fail_fast` argument indicates whether not yet executed scenarios should
    /// be omitted.
    ///
    /// [`Feature`]: gherkin::Feature
    async fn is_finished(&self, fail_fast: bool) -> bool {
        self.finished.load(Ordering::SeqCst)
            && (fail_fast
                || self.scenarios.lock().await.values().all(Vec::is_empty))
    }
}

/// Coerces the given `value` into a type-erased [`Info`].
fn coerce_into_info<T: std::any::Any + Send + 'static>(val: T) -> Info {
    Arc::new(val)
}

/// Failure encountered during execution of [`HookType::Before`] or [`Step`].
/// See [`Executor::emit_failed_events()`] for more info.
///
/// [`Step`]: gherkin::Step
enum ExecutionFailure<World> {
    /// [`HookType::Before`] panicked.
    BeforeHookPanicked {
        /// [`World`] at the time [`HookType::Before`] has panicked.
        world: Option<World>,

        /// [`catch_unwind()`] of the [`HookType::Before`] panic.
        ///
        /// [`catch_unwind()`]: std::panic::catch_unwind
        panic_info: Info,

        /// [`Metadata`] at the time [`HookType::Before`] panicked.
        ///
        /// [`Metadata`]: event::Metadata
        meta: event::Metadata,
    },

    /// [`Step`] was skipped.
    ///
    /// [`Step`]: gherkin::Step.
    StepSkipped(Option<World>),

    /// [`Step`] failed.
    ///
    /// [`Step`]: gherkin::Step.
    StepPanicked {
        /// [`World`] at the time when [`Step`] has failed.
        ///
        /// [`Step`]: gherkin::Step
        world: Option<World>,

        /// [`Step`] itself.
        ///
        /// [`Step`]: gherkin::Step
        step: Arc<gherkin::Step>,

        /// [`Step`]s [`regex`] [`CaptureLocations`].
        ///
        /// [`Step`]: gherkin::Step
        captures: Option<CaptureLocations>,

        /// [`Location`] of the [`fn`] that matched this [`Step`].
        ///
        /// [`Location`]: step::Location
        /// [`Step`]: gherkin::Step
        loc: Option<step::Location>,

        /// [`StepError`] of the [`Step`].
        ///
        /// [`Step`]: gherkin::Step
        /// [`StepError`]: event::StepError
        err: event::StepError,

        /// [`Metadata`] at the time when [`Step`] failed.
        ///
        /// [`Metadata`]: event::Metadata
        /// [`Step`]: gherkin::Step.
        meta: event::Metadata,

        /// Indicator whether the [`Step`] was background or not.
        ///
        /// [`Step`]: gherkin::Step
        is_background: bool,
    },
}

/// [`Metadata`] of [`HookType::After`] events.
///
/// [`Metadata`]: event::Metadata
struct AfterHookEventsMeta {
    /// [`Metadata`] at the time [`HookType::After`] started.
    ///
    /// [`Metadata`]: event::Metadata
    started: event::Metadata,

    /// [`Metadata`] at the time [`HookType::After`] finished.
    ///
    /// [`Metadata`]: event::Metadata
    finished: event::Metadata,
}

impl<W> ExecutionFailure<W> {
    /// Takes the [`World`] leaving a [`None`] in its place.
    fn take_world(&mut self) -> Option<W> {
        match self {
            Self::BeforeHookPanicked { world, .. }
            | Self::StepSkipped(world)
            | Self::StepPanicked { world, .. } => world.take(),
        }
    }

    /// Creates an [`event::ScenarioFinished`] from this [`ExecutionFailure`].
    fn get_scenario_finished_event(&self) -> event::ScenarioFinished {
        use event::ScenarioFinished::{
            BeforeHookFailed, StepFailed, StepSkipped,
        };

        match self {
            Self::BeforeHookPanicked { panic_info, .. } => {
                BeforeHookFailed(Arc::clone(panic_info))
            }
            Self::StepSkipped(_) => StepSkipped,
            Self::StepPanicked {
                captures, loc, err, ..
            } => StepFailed(captures.clone(), *loc, err.clone()),
        }
    }
}

#[cfg(test)]
mod retry_options {
    use gherkin::GherkinEnv;
    use humantime::parse_duration;

    use super::{Cli, Duration, Retries, RetryOptions};

    mod scenario_tags {
        use super::*;

        // language=Gherkin
        const FEATURE: &str = r"
Feature: only scenarios
  Scenario: no tags
    Given a step

  @retry
  Scenario: tag
    Given a step

  @retry(5)
  Scenario: tag with explicit value
    Given a step

  @retry.after(3s)
  Scenario: tag with explicit after
    Given a step

  @retry(5).after(15s)
  Scenario: tag with explicit value and after
    Given a step
";

        #[test]
        fn empty_cli() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: None,
                retry_after: None,
                retry_tag_filter: None,
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                None,
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }

        #[test]
        fn cli_retries() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: Some(7),
                retry_after: None,
                retry_tag_filter: None,
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }

        #[test]
        fn cli_retry_after() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: Some(7),
                retry_after: Some(parse_duration("5s").unwrap()),
                retry_tag_filter: None,
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }

        #[test]
        fn cli_retry_filter() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: Some(7),
                retry_after: None,
                retry_tag_filter: Some("@retry".parse().unwrap()),
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                None,
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }

        #[test]
        fn cli_retry_after_and_filter() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: Some(7),
                retry_after: Some(parse_duration("5s").unwrap()),
                retry_tag_filter: Some("@retry".parse().unwrap()),
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                None,
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }
    }

    mod rule_tags {
        use super::*;

        // language=Gherkin
        const FEATURE: &str = r#"
Feature: only scenarios
  Rule: no tags
    Scenario: no tags
      Given a step

    @retry
    Scenario: tag
      Given a step

    @retry(5)
    Scenario: tag with explicit value
      Given a step

    @retry.after(3s)
    Scenario: tag with explicit after
      Given a step

    @retry(5).after(15s)
    Scenario: tag with explicit value and after
      Given a step

  @retry(3).after(5s)
  Rule: retry tag
    Scenario: no tags
      Given a step

    @retry
    Scenario: tag
      Given a step

    @retry(5)
    Scenario: tag with explicit value
      Given a step

    @retry.after(3s)
    Scenario: tag with explicit after
      Given a step

    @retry(5).after(15s)
    Scenario: tag with explicit value and after
      Given a step
"#;

        #[test]
        fn empty_cli() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: None,
                retry_after: None,
                retry_tag_filter: None,
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[0],
                    &cli
                ),
                None,
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[0],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 3,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }

        #[test]
        fn cli_retry_after_and_filter() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: Some(7),
                retry_after: Some(parse_duration("5s").unwrap()),
                retry_tag_filter: Some("@retry".parse().unwrap()),
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[0],
                    &cli
                ),
                None,
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[0],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 3,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }
    }

    mod feature_tags {
        use super::*;

        // language=Gherkin
        const FEATURE: &str = r"
@retry(8)
Feature: only scenarios
  Scenario: no tags
    Given a step

  @retry
  Scenario: tag
    Given a step

  @retry(5)
  Scenario: tag with explicit value
    Given a step

  @retry.after(3s)
  Scenario: tag with explicit after
    Given a step

  @retry(5).after(15s)
  Scenario: tag with explicit value and after
    Given a step

  Rule: no tags
    Scenario: no tags
      Given a step

    @retry
    Scenario: tag
      Given a step

    @retry(5)
    Scenario: tag with explicit value
      Given a step

    @retry.after(3s)
    Scenario: tag with explicit after
      Given a step

    @retry(5).after(15s)
    Scenario: tag with explicit value and after
      Given a step

  @retry(3).after(5s)
  Rule: retry tag
    Scenario: no tags
      Given a step

    @retry
    Scenario: tag
      Given a step

    @retry(5)
    Scenario: tag with explicit value
      Given a step

    @retry.after(3s)
    Scenario: tag with explicit after
      Given a step

    @retry(5).after(15s)
    Scenario: tag with explicit value and after
      Given a step
";

        #[test]
        fn empty_cli() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: None,
                retry_after: None,
                retry_tag_filter: None,
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .unwrap_or_else(|e| panic!("failed to parse feature: {e}"));

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 8,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[0],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 8,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[0],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 3,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: None,
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 1,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }

        #[test]
        fn cli_retry_after_and_filter() {
            let cli = Cli {
                concurrency: None,
                fail_fast: false,
                retry: Some(7),
                retry_after: Some(parse_duration("5s").unwrap()),
                retry_tag_filter: Some("@retry".parse().unwrap()),
            };
            let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
                .expect("failed to parse feature");

            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 8,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[0],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 8,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[0]),
                    &f.rules[0].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[0],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 3,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[1],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[2],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(5)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[3],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 7,
                    },
                    after: Some(Duration::from_secs(3)),
                }),
            );
            assert_eq!(
                RetryOptions::parse_from_tags(
                    &f,
                    Some(&f.rules[1]),
                    &f.rules[1].scenarios[4],
                    &cli
                ),
                Some(RetryOptions {
                    retries: Retries {
                        current: 0,
                        left: 5,
                    },
                    after: Some(Duration::from_secs(15)),
                }),
            );
        }
    }
}