use std::{
borrow::Cow,
fmt::{Debug, Formatter},
marker::PhantomData,
mem,
path::Path,
time::Duration,
};
use futures::{future::LocalBoxFuture, StreamExt as _};
use gherkin::tagexpr::TagOperation;
use regex::Regex;
use crate::{
cli, event, parser,
runner::{self, basic::RetryOptions},
step,
tag::Ext as _,
writer, Event, Parser, Runner, ScenarioType, Step, World, Writer,
WriterExt as _,
};
pub struct Cucumber<W, P, I, R, Wr, Cli = cli::Empty>
where
W: World,
P: Parser<I>,
R: Runner<W>,
Wr: Writer<W>,
Cli: clap::Args,
{
parser: P,
runner: R,
writer: Wr,
#[allow(clippy::type_complexity)] cli: Option<cli::Opts<P::Cli, R::Cli, Wr::Cli, Cli>>,
_world: PhantomData<W>,
_parser_input: PhantomData<I>,
}
impl<W, P, I, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
where
W: World,
P: Parser<I>,
R: Runner<W>,
Wr: Writer<W>,
Cli: clap::Args,
{
#[must_use]
pub const fn custom(parser: P, runner: R, writer: Wr) -> Self {
Self {
parser,
runner,
writer,
cli: None,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn with_parser<NewP, NewI>(
self,
parser: NewP,
) -> Cucumber<W, NewP, NewI, R, Wr, Cli>
where
NewP: Parser<NewI>,
{
let Self { runner, writer, .. } = self;
Cucumber {
parser,
runner,
writer,
cli: None,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn with_runner<NewR>(
self,
runner: NewR,
) -> Cucumber<W, P, I, NewR, Wr, Cli>
where
NewR: Runner<W>,
{
let Self { parser, writer, .. } = self;
Cucumber {
parser,
runner,
writer,
cli: None,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn with_writer<NewWr>(
self,
writer: NewWr,
) -> Cucumber<W, P, I, R, NewWr, Cli>
where
NewWr: Writer<W>,
{
let Self { parser, runner, .. } = self;
Cucumber {
parser,
runner,
writer,
cli: None,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn repeat_skipped(
self,
) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr>, Cli>
where
Wr: writer::NonTransforming,
{
Cucumber {
parser: self.parser,
runner: self.runner,
writer: self.writer.repeat_skipped(),
cli: self.cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn repeat_failed(
self,
) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr>, Cli>
where
Wr: writer::NonTransforming,
{
Cucumber {
parser: self.parser,
runner: self.runner,
writer: self.writer.repeat_failed(),
cli: self.cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn repeat_if<F>(
self,
filter: F,
) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr, F>, Cli>
where
F: Fn(&parser::Result<Event<event::Cucumber<W>>>) -> bool,
Wr: writer::NonTransforming,
{
Cucumber {
parser: self.parser,
runner: self.runner,
writer: self.writer.repeat_if(filter),
cli: self.cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn fail_on_skipped(
self,
) -> Cucumber<W, P, I, R, writer::FailOnSkipped<Wr>, Cli> {
Cucumber {
parser: self.parser,
runner: self.runner,
writer: self.writer.fail_on_skipped(),
cli: self.cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn fail_on_skipped_with<Filter>(
self,
filter: Filter,
) -> Cucumber<W, P, I, R, writer::FailOnSkipped<Wr, Filter>, Cli>
where
Filter: Fn(
&gherkin::Feature,
Option<&gherkin::Rule>,
&gherkin::Scenario,
) -> bool,
{
Cucumber {
parser: self.parser,
runner: self.runner,
writer: self.writer.fail_on_skipped_with(filter),
cli: self.cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
}
impl<W, P, I, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
where
W: World,
P: Parser<I>,
R: Runner<W>,
Wr: Writer<W> + writer::Normalized,
Cli: clap::Args,
{
pub async fn run(self, input: I) -> Wr {
self.filter_run(input, |_, _, _| true).await
}
#[allow(clippy::missing_const_for_fn)] pub fn with_cli<CustomCli>(
self,
cli: cli::Opts<P::Cli, R::Cli, Wr::Cli, CustomCli>,
) -> Cucumber<W, P, I, R, Wr, CustomCli>
where
CustomCli: clap::Args,
{
let Self {
parser,
runner,
writer,
..
} = self;
Cucumber {
parser,
runner,
writer,
cli: Some(cli),
_world: PhantomData,
_parser_input: PhantomData,
}
}
pub async fn filter_run<F>(self, input: I, filter: F) -> Wr
where
F: Fn(
&gherkin::Feature,
Option<&gherkin::Rule>,
&gherkin::Scenario,
) -> bool
+ 'static,
{
let cli::Opts {
re_filter,
tags_filter,
parser: parser_cli,
runner: runner_cli,
writer: writer_cli,
..
} = self.cli.unwrap_or_else(cli::Opts::<_, _, _, _>::parsed);
let filter = move |feat: &gherkin::Feature,
rule: Option<&gherkin::Rule>,
scenario: &gherkin::Scenario| {
re_filter.as_ref().map_or_else(
|| {
tags_filter.as_ref().map_or_else(
|| filter(feat, rule, scenario),
|tags| {
tags.eval(
feat.tags
.iter()
.chain(
rule.into_iter()
.flat_map(|r| r.tags.iter()),
)
.chain(scenario.tags.iter()),
)
},
)
},
|re| re.is_match(&scenario.name),
)
};
let Self {
parser,
runner,
mut writer,
..
} = self;
let features = parser.parse(input, parser_cli);
let filtered = features.map(move |feature| {
let mut feature = feature?;
let feat_scenarios = mem::take(&mut feature.scenarios);
feature.scenarios = feat_scenarios
.into_iter()
.filter(|s| filter(&feature, None, s))
.collect();
let mut rules = mem::take(&mut feature.rules);
for r in &mut rules {
let rule_scenarios = mem::take(&mut r.scenarios);
r.scenarios = rule_scenarios
.into_iter()
.filter(|s| filter(&feature, Some(r), s))
.collect();
}
feature.rules = rules;
Ok(feature)
});
let events_stream = runner.run(filtered, runner_cli);
futures::pin_mut!(events_stream);
while let Some(ev) = events_stream.next().await {
writer.handle_event(ev, &writer_cli).await;
}
writer
}
}
impl<W, P, I, R, Wr, Cli> Debug for Cucumber<W, P, I, R, Wr, Cli>
where
W: World,
P: Debug + Parser<I>,
R: Debug + Runner<W>,
Wr: Debug + Writer<W>,
Cli: clap::Args,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cucumber")
.field("parser", &self.parser)
.field("runner", &self.runner)
.field("writer", &self.writer)
.finish()
}
}
pub(crate) type DefaultCucumber<W, I> = Cucumber<
W,
parser::Basic,
I,
runner::Basic<W>,
writer::Summarize<writer::Normalize<W, writer::Basic>>,
>;
impl<W, I> Default for DefaultCucumber<W, I>
where
W: World + Debug,
I: AsRef<Path>,
{
fn default() -> Self {
Self::custom(
parser::Basic::new(),
runner::Basic::default(),
writer::Basic::stdout().summarized(),
)
}
}
impl<W, I> DefaultCucumber<W, I>
where
W: World + Debug,
I: AsRef<Path>,
{
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl<W, I, R, Wr, Cli> Cucumber<W, parser::Basic, I, R, Wr, Cli>
where
W: World,
R: Runner<W>,
Wr: Writer<W>,
Cli: clap::Args,
I: AsRef<Path>,
{
pub fn language(
mut self,
name: impl Into<Cow<'static, str>>,
) -> Result<Self, parser::basic::UnsupportedLanguageError> {
self.parser = self.parser.language(name)?;
Ok(self)
}
}
impl<W, I, P, Wr, F, B, A, Cli>
Cucumber<W, P, I, runner::Basic<W, F, B, A>, Wr, Cli>
where
W: World,
P: Parser<I>,
Wr: Writer<W>,
Cli: clap::Args,
F: Fn(
&gherkin::Feature,
Option<&gherkin::Rule>,
&gherkin::Scenario,
) -> ScenarioType
+ 'static,
B: for<'a> Fn(
&'a gherkin::Feature,
Option<&'a gherkin::Rule>,
&'a gherkin::Scenario,
&'a mut W,
) -> LocalBoxFuture<'a, ()>
+ 'static,
A: for<'a> Fn(
&'a gherkin::Feature,
Option<&'a gherkin::Rule>,
&'a gherkin::Scenario,
Option<&'a mut W>,
) -> LocalBoxFuture<'a, ()>
+ 'static,
{
#[must_use]
pub fn max_concurrent_scenarios(
mut self,
max: impl Into<Option<usize>>,
) -> Self {
self.runner = self.runner.max_concurrent_scenarios(max);
self
}
#[must_use]
pub fn retries(mut self, retries: impl Into<Option<usize>>) -> Self {
self.runner = self.runner.retries(retries);
self
}
#[must_use]
pub fn retry_after(mut self, after: impl Into<Option<Duration>>) -> Self {
self.runner = self.runner.retry_after(after);
self
}
#[must_use]
pub fn retry_filter(
mut self,
tag_expression: impl Into<Option<TagOperation>>,
) -> Self {
self.runner = self.runner.retry_filter(tag_expression);
self
}
#[must_use]
pub fn which_scenario<Which>(
self,
func: Which,
) -> Cucumber<W, P, I, runner::Basic<W, Which, B, A>, Wr, Cli>
where
Which: Fn(
&gherkin::Feature,
Option<&gherkin::Rule>,
&gherkin::Scenario,
) -> ScenarioType
+ 'static,
{
let Self {
parser,
runner,
writer,
cli,
..
} = self;
Cucumber {
parser,
runner: runner.which_scenario(func),
writer,
cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn retry_options<Retry>(mut self, func: Retry) -> Self
where
Retry: Fn(
&gherkin::Feature,
Option<&gherkin::Rule>,
&gherkin::Scenario,
&runner::basic::Cli,
) -> Option<RetryOptions>
+ 'static,
{
self.runner = self.runner.retry_options(func);
self
}
#[must_use]
pub fn before<Before>(
self,
func: Before,
) -> Cucumber<W, P, I, runner::Basic<W, F, Before, A>, Wr, Cli>
where
Before: for<'a> Fn(
&'a gherkin::Feature,
Option<&'a gherkin::Rule>,
&'a gherkin::Scenario,
&'a mut W,
) -> LocalBoxFuture<'a, ()>
+ 'static,
{
let Self {
parser,
runner,
writer,
cli,
..
} = self;
Cucumber {
parser,
runner: runner.before(func),
writer,
cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn after<After>(
self,
func: After,
) -> Cucumber<W, P, I, runner::Basic<W, F, B, After>, Wr, Cli>
where
After: for<'a> Fn(
&'a gherkin::Feature,
Option<&'a gherkin::Rule>,
&'a gherkin::Scenario,
Option<&'a mut W>,
) -> LocalBoxFuture<'a, ()>
+ 'static,
{
let Self {
parser,
runner,
writer,
cli,
..
} = self;
Cucumber {
parser,
runner: runner.after(func),
writer,
cli,
_world: PhantomData,
_parser_input: PhantomData,
}
}
#[must_use]
pub fn steps(mut self, steps: step::Collection<W>) -> Self {
self.runner = self.runner.steps(steps);
self
}
#[must_use]
pub fn given(mut self, regex: Regex, step: Step<W>) -> Self {
self.runner = self.runner.given(regex, step);
self
}
#[must_use]
pub fn when(mut self, regex: Regex, step: Step<W>) -> Self {
self.runner = self.runner.when(regex, step);
self
}
#[must_use]
pub fn then(mut self, regex: Regex, step: Step<W>) -> Self {
self.runner = self.runner.then(regex, step);
self
}
}
impl<W, I, P, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
where
W: World,
P: Parser<I>,
R: Runner<W>,
Wr: writer::Stats<W> + writer::Normalized,
Cli: clap::Args,
{
pub async fn run_and_exit(self, input: I) {
self.filter_run_and_exit(input, |_, _, _| true).await;
}
pub async fn filter_run_and_exit<Filter>(self, input: I, filter: Filter)
where
Filter: Fn(
&gherkin::Feature,
Option<&gherkin::Rule>,
&gherkin::Scenario,
) -> bool
+ 'static,
{
let writer = self.filter_run(input, filter).await;
if writer.execution_has_failed() {
let mut msg = Vec::with_capacity(3);
let failed_steps = writer.failed_steps();
if failed_steps > 0 {
msg.push(format!(
"{failed_steps} step{} failed",
(failed_steps > 1).then_some("s").unwrap_or_default(),
));
}
let parsing_errors = writer.parsing_errors();
if parsing_errors > 0 {
msg.push(format!(
"{parsing_errors} parsing error{}",
(parsing_errors > 1).then_some("s").unwrap_or_default(),
));
}
let hook_errors = writer.hook_errors();
if hook_errors > 0 {
msg.push(format!(
"{hook_errors} hook error{}",
(hook_errors > 1).then_some("s").unwrap_or_default(),
));
}
panic!("{}", msg.join(", "));
}
}
}