From f031fc29acec162989317f07e26690993c6dd953 Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 20 Oct 2021 10:35:54 +0300 Subject: [PATCH 01/27] Add --verbose flag and remove legacy CLI options --- Cargo.toml | 3 +- src/cli.rs | 52 ++-- src/cucumber.rs | 36 ++- src/parser/basic.rs | 6 +- src/parser/mod.rs | 15 +- src/runner/basic.rs | 5 +- src/runner/mod.rs | 15 +- src/writer/basic.rs | 239 ++++++++++++------ src/writer/fail_on_skipped.rs | 10 +- src/writer/mod.rs | 14 + src/writer/normalized.rs | 68 +++-- src/writer/repeat.rs | 12 +- src/writer/summarized.rs | 10 +- .../output/ambiguous_step.feature.out | 2 +- tests/features/wait/outline.feature | 3 + tests/output.rs | 7 +- 16 files changed, 347 insertions(+), 150 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bd91588..2aff08ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ macros = ["cucumber-codegen", "inventory"] [dependencies] async-trait = "0.1.40" atty = "0.2.14" -clap = { version = "=3.0.0-beta.5", features = ["derive"] } +clap = "2.33" console = "0.15" derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false } either = "1.6" @@ -43,6 +43,7 @@ linked-hash-map = "0.5.3" once_cell = { version = "1.8", features = ["parking_lot"] } regex = "1.5" sealed = "0.3" +structopt = "0.3.25" # "macros" feature dependencies cucumber-codegen = { version = "0.10", path = "./codegen", optional = true } diff --git a/src/cli.rs b/src/cli.rs index 0106fb09..767e6595 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,28 +11,44 @@ //! CLI options. use regex::Regex; +use structopt::StructOpt; /// Run the tests, pet a dog!. -/// -/// __WARNING__ ⚠️: This CLI exists only for backwards compatibility. In `0.11` -/// it will be completely reworked: -/// [cucumber-rs/cucumber#134][1]. -/// -/// [1]: https://github.com/cucumber-rs/cucumber/issues/134 -#[derive(clap::Parser, Debug)] -pub struct Opts { +#[derive(StructOpt, Debug)] +pub struct Opts +where + Parser: StructOpt, + Runner: StructOpt, + Writer: StructOpt, +{ /// Regex to select scenarios from. - #[clap(short = 'e', long = "expression", name = "regex")] + #[structopt(short = "e", long = "expression", name = "regex")] pub filter: Option, - /// __WARNING__ ⚠️: This option does nothing at the moment and is deprecated - /// for removal in the next major release. - /// Any output of step functions is not captured by default. - #[clap(long)] - pub nocapture: bool, + /// [`Parser`] CLI options. + /// + /// [`Parser`]: crate::Parser + #[structopt(flatten)] + pub parser: Parser, - /// __WARNING__ ⚠️: This option does nothing at the moment and is deprecated - /// for removal in the next major release. - #[clap(long)] - pub debug: bool, + /// [`Runner`] CLI options. + /// + /// [`Runner`]: crate::Runner + #[structopt(flatten)] + pub runner: Runner, + + /// [`Writer`] CLI options. + /// + /// [`Writer`]: crate::Writer + #[structopt(flatten)] + pub writer: Writer, +} + +/// Empty CLI options. +#[derive(StructOpt, Clone, Copy, Debug)] +pub struct Empty { + /// This field exists only because [`StructOpt`] derive macro doesn't + /// support unit structs. + #[structopt(skip)] + skipped: (), } diff --git a/src/cucumber.rs b/src/cucumber.rs index f0451049..133c29f9 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -20,9 +20,9 @@ use std::{ path::Path, }; -use clap::Parser as _; use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; +use structopt::StructOpt as _; use crate::{ cli, event, parser, runner, step, writer, ArbitraryWriter, FailureWriter, @@ -682,25 +682,21 @@ where ) -> bool + 'static, { - let opts = cli::Opts::parse(); - if opts.nocapture { - eprintln!( - "WARNING ⚠️: This option does nothing at the moment and is \ - deprecated for removal in the next major release. \ - Any output of step functions is not captured by \ - default.", - ); - } - if opts.debug { - eprintln!( - "WARNING ⚠️: This option does nothing at the moment and is \ - deprecated for removal in the next major release.", - ); - } + let (filter_cli, parser_cli, runner_cli, writer_cli) = { + let cli::Opts { + filter, + parser, + runner, + writer, + } = cli::Opts::::from_args(); + + (filter, parser, runner, writer) + }; + let filter = move |f: &gherkin::Feature, r: Option<&gherkin::Rule>, s: &gherkin::Scenario| { - opts.filter + filter_cli .as_ref() .map_or_else(|| filter(f, r, s), |f| f.is_match(&s.name)) }; @@ -712,7 +708,7 @@ where .. } = self; - let features = parser.parse(input); + let features = parser.parse(input, parser_cli); let filtered = features.map(move |feature| { let mut feature = feature?; @@ -735,10 +731,10 @@ where Ok(feature) }); - let events_stream = runner.run(filtered); + 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).await; + writer.handle_event(ev, &writer_cli).await; } writer } diff --git a/src/parser/basic.rs b/src/parser/basic.rs index 4a67f33d..b7138896 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -22,7 +22,7 @@ use futures::stream; use gherkin::GherkinEnv; use globwalk::GlobWalkerBuilder; -use crate::feature::Ext as _; +use crate::{cli, feature::Ext as _}; use super::{Error as ParseError, Parser}; @@ -39,10 +39,12 @@ pub struct Basic { } impl> Parser for Basic { + type CLI = cli::Empty; + type Output = stream::Iter>>; - fn parse(self, path: I) -> Self::Output { + fn parse(self, path: I, _: cli::Empty) -> Self::Output { let features = || { let path = path.as_ref(); let path = match path.canonicalize().or_else(|_| { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5e32d24e..ca5b714f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18,6 +18,7 @@ use std::sync::Arc; use derive_more::{Display, Error, From}; use futures::Stream; +use structopt::StructOptInternal; use crate::feature::ExpandExamplesError; @@ -28,6 +29,18 @@ pub use self::basic::Basic; /// /// [`Feature`]: gherkin::Feature pub trait Parser { + /// [`StructOpt`] deriver for CLI options of this [`Parser`]. In case no + /// options present, use [`cli::Empty`]. + /// + /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be + /// merged together, so overlapping arguments will cause runtime panic. + /// + /// [`cli::Empty`]: crate::cli::Empty + /// [`Runner`]: crate::Runner + /// [`StructOpt`]: structopt::StructOpt + /// [`Writer`]: crate::Writer + type CLI: StructOptInternal + 'static; + /// Output [`Stream`] of parsed [`Feature`]s. /// /// [`Feature`]: gherkin::Feature @@ -36,7 +49,7 @@ pub trait Parser { /// Parses the given `input` into a [`Stream`] of [`Feature`]s. /// /// [`Feature`]: gherkin::Feature - fn parse(self, input: I) -> Self::Output; + fn parse(self, input: I, cli: Self::CLI) -> Self::Output; } /// Result of parsing [Gherkin] files. diff --git a/src/runner/basic.rs b/src/runner/basic.rs index f3bb5645..fa4b643f 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -36,6 +36,7 @@ use itertools::Itertools as _; use regex::{CaptureLocations, Regex}; use crate::{ + cli, event::{self, HookType}, feature::Ext as _, parser, step, Runner, Step, World, @@ -367,10 +368,12 @@ where ) -> LocalBoxFuture<'a, ()> + 'static, { + type CLI = cli::Empty; + type EventStream = LocalBoxStream<'static, parser::Result>>; - fn run(self, features: S) -> Self::EventStream + fn run(self, features: S, _: cli::Empty) -> Self::EventStream where S: Stream> + 'static, { diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 870acb3f..6af4f4d5 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -17,6 +17,7 @@ pub mod basic; use futures::Stream; +use structopt::StructOptInternal; use crate::{event, parser}; @@ -59,6 +60,18 @@ pub use self::basic::{Basic, ScenarioType}; /// /// [happened-before]: https://en.wikipedia.org/wiki/Happened-before pub trait Runner { + /// [`StructOpt`] deriver for CLI options of this [`Runner`]. In case no + /// options present, use [`cli::Empty`]. + /// + /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be + /// merged together, so overlapping arguments will cause runtime panic. + /// + /// [`cli::Empty`]: crate::cli::Empty + /// [`Parser`]: crate::Parser + /// [`StructOpt`]: structopt::StructOpt + /// [`Writer`]: crate::Writer + type CLI: StructOptInternal; + /// Output events [`Stream`]. type EventStream: Stream>>; @@ -67,7 +80,7 @@ pub trait Runner { /// /// [`Cucumber`]: event::Cucumber /// [`Feature`]: gherkin::Feature - fn run(self, features: S) -> Self::EventStream + fn run(self, features: S, cli: Self::CLI) -> Self::EventStream where S: Stream> + 'static; } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index dc247bec..809c09ab 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -21,6 +21,7 @@ use async_trait::async_trait; use console::Term; use itertools::Itertools as _; use regex::CaptureLocations; +use structopt::StructOpt; use crate::{ event::{self, Info}, @@ -49,10 +50,24 @@ pub struct Basic { lines_to_clear: usize, } +/// CLI options of [`Basic`]. +#[derive(Clone, Copy, Debug, StructOpt)] +pub struct CLI { + /// Outputs Doc String, if present. + #[structopt(long)] + pub verbose: bool, +} + #[async_trait(?Send)] impl Writer for Basic { + type CLI = CLI; + #[allow(clippy::unused_async)] - async fn handle_event(&mut self, ev: parser::Result>) { + async fn handle_event( + &mut self, + ev: parser::Result>, + cli: &Self::CLI, + ) { use event::{Cucumber, Feature}; match ev { @@ -60,8 +75,8 @@ impl Writer for Basic { Ok(Cucumber::Started | Cucumber::Finished) => {} Ok(Cucumber::Feature(f, ev)) => match ev { Feature::Started => self.feature_started(&f), - Feature::Scenario(sc, ev) => self.scenario(&f, &sc, &ev), - Feature::Rule(r, ev) => self.rule(&f, &r, ev), + Feature::Scenario(sc, ev) => self.scenario(&f, &sc, &ev, *cli), + Feature::Rule(r, ev) => self.rule(&f, &r, ev, *cli), Feature::Finished => {} }, } @@ -152,6 +167,7 @@ impl Basic { feat: &gherkin::Feature, rule: &gherkin::Rule, ev: event::Rule, + cli: CLI, ) { use event::Rule; @@ -160,7 +176,7 @@ impl Basic { self.rule_started(rule); } Rule::Scenario(sc, ev) => { - self.scenario(feat, &sc, &ev); + self.scenario(feat, &sc, &ev, cli); } Rule::Finished => { self.indent = self.indent.saturating_sub(2); @@ -195,6 +211,7 @@ impl Basic { feat: &gherkin::Feature, scenario: &gherkin::Scenario, ev: &event::Scenario, + cli: CLI, ) { use event::{Hook, Scenario}; @@ -213,10 +230,10 @@ impl Basic { self.indent = self.indent.saturating_sub(4); } Scenario::Background(bg, ev) => { - self.background(feat, bg, ev); + self.background(feat, bg, ev, cli); } Scenario::Step(st, ev) => { - self.step(feat, st, ev); + self.step(feat, st, ev, cli); } Scenario::Finished => self.indent = self.indent.saturating_sub(2), } @@ -237,19 +254,22 @@ impl Basic { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.err(format!( - "{indent}\u{2718} Scenario's {} hook failed {}:{}:{}\n\ + "{indent}\u{2718} Scenario's {} hook failed {}:{}:{}\n\ {indent} Captured output: {}{}", - which, - feat.path - .as_ref() - .and_then(|p| p.to_str()) - .unwrap_or(&feat.name), - sc.position.line, - sc.position.col, - coerce_error(info), - format_debug_with_indent(world, self.indent.saturating_sub(3) + 3), - indent = " ".repeat(self.indent.saturating_sub(3)), - ))) + which, + feat.path + .as_ref() + .and_then(|p| p.to_str()) + .unwrap_or(&feat.name), + sc.position.line, + sc.position.col, + coerce_error(info), + format_str_with_indent( + world.map(|w| format!("{:#?}", w)).as_deref(), + self.indent.saturating_sub(3) + 3 + ), + indent = " ".repeat(self.indent.saturating_sub(3)), + ))) .unwrap(); } @@ -281,23 +301,24 @@ impl Basic { feat: &gherkin::Feature, step: &gherkin::Step, ev: &event::Step, + cli: CLI, ) { use event::Step; match ev { Step::Started => { - self.step_started(step); + self.step_started(step, cli); } Step::Passed(captures) => { - self.step_passed(step, captures); + self.step_passed(step, captures, cli); self.indent = self.indent.saturating_sub(4); } Step::Skipped => { - self.step_skipped(feat, step); + self.step_skipped(feat, step, cli); self.indent = self.indent.saturating_sub(4); } - Step::Failed(capts, w, info) => { - self.step_failed(feat, step, capts.as_ref(), w.as_ref(), info); + Step::Failed(c, w, info) => { + self.step_failed(feat, step, c.as_ref(), w.as_ref(), info, cli); self.indent = self.indent.saturating_sub(4); } } @@ -313,13 +334,22 @@ impl Basic { /// [skipped]: event::Step::Skipped /// [started]: event::Step::Started /// [`Step`]: gherkin::Step - fn step_started(&mut self, step: &gherkin::Step) { + fn step_started(&mut self, step: &gherkin::Step, cli: CLI) { self.indent += 4; if self.styles.is_present { let output = format!( - "{indent}{} {}{}", + "{indent}{} {}{}{}", step.keyword, step.value, + step.docstring + .as_ref() + .and_then(|doc| cli.verbose.then( + || format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ) + )) + .unwrap_or_default(), step.table .as_ref() .map(|t| format_table(t, self.indent)) @@ -339,6 +369,7 @@ impl Basic { &mut self, step: &gherkin::Step, captures: &CaptureLocations, + cli: CLI, ) { self.clear_last_lines_if_term_present(); @@ -350,6 +381,18 @@ impl Basic { |v| self.styles.ok(v), |v| self.styles.ok(self.styles.bold(v)), ); + let doc_str = self.styles.ok(step + .docstring + .as_ref() + .and_then(|doc| { + cli.verbose.then(|| { + format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ) + }) + }) + .unwrap_or_default()); let step_table = self.styles.ok(step .table .as_ref() @@ -357,9 +400,10 @@ impl Basic { .unwrap_or_default()); self.write_line(&self.styles.ok(format!( - "{indent}{} {}{}", + "{indent}{} {}{}{}", step_keyword, step_value, + doc_str, step_table, indent = " ".repeat(self.indent.saturating_sub(3)), ))) @@ -370,13 +414,25 @@ impl Basic { /// /// [skipped]: event::Step::Skipped /// [`Step`]: gherkin::Step - fn step_skipped(&mut self, feat: &gherkin::Feature, step: &gherkin::Step) { + fn step_skipped( + &mut self, + feat: &gherkin::Feature, + step: &gherkin::Step, + cli: CLI, + ) { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.skipped(format!( - "{indent}? {} {}{}\n\ + "{indent}? {} {}{}{}{}\n\ {indent} Step skipped: {}:{}:{}", step.keyword, step.value, + step.docstring + .as_ref() + .and_then(|doc| cli.verbose.then(|| format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ))) + .unwrap_or_default(), step.table .as_ref() .map(|t| format_table(t, self.indent)) @@ -403,6 +459,7 @@ impl Basic { captures: Option<&CaptureLocations>, world: Option<&W>, err: &event::StepError, + cli: CLI, ) { self.clear_last_lines_if_term_present(); @@ -425,9 +482,16 @@ impl Basic { ); let diagnostics = self.styles.err(format!( - "{}\n\ + "{}{}\n\ {indent} Step failed: {}:{}:{}\n\ {indent} Captured output: {}{}", + step.docstring + .as_ref() + .and_then(|doc| cli.verbose.then(|| format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ))) + .unwrap_or_default(), step.table .as_ref() .map(|t| format_table(t, self.indent)) @@ -438,8 +502,14 @@ impl Basic { .unwrap_or(&feat.name), step.position.line, step.position.col, - format_display_with_indent(err, self.indent.saturating_sub(3) + 3), - format_debug_with_indent(world, self.indent.saturating_sub(3) + 3), + format_str_with_indent( + format!("{}", err).as_str(), + self.indent.saturating_sub(3) + 3 + ), + format_str_with_indent( + world.map(|w| format!("{:#?}", w)).as_deref(), + self.indent.saturating_sub(3) + 3 + ), indent = " ".repeat(self.indent.saturating_sub(3)) )); @@ -464,23 +534,24 @@ impl Basic { feat: &gherkin::Feature, bg: &gherkin::Step, ev: &event::Step, + cli: CLI, ) { use event::Step; match ev { Step::Started => { - self.bg_step_started(bg); + self.bg_step_started(bg, cli); } Step::Passed(captures) => { - self.bg_step_passed(bg, captures); + self.bg_step_passed(bg, captures, cli); self.indent = self.indent.saturating_sub(4); } Step::Skipped => { - self.bg_step_skipped(feat, bg); + self.bg_step_skipped(feat, bg, cli); self.indent = self.indent.saturating_sub(4); } - Step::Failed(capts, w, info) => { - self.bg_step_failed(feat, bg, capts.as_ref(), w.as_ref(), info); + Step::Failed(c, w, i) => { + self.bg_step_failed(feat, bg, c.as_ref(), w.as_ref(), i, cli); self.indent = self.indent.saturating_sub(4); } } @@ -497,13 +568,22 @@ impl Basic { /// [started]: event::Step::Started /// [`Background`]: gherkin::Background /// [`Step`]: gherkin::Step - fn bg_step_started(&mut self, step: &gherkin::Step) { + fn bg_step_started(&mut self, step: &gherkin::Step, cli: CLI) { self.indent += 4; if self.styles.is_present { let output = format!( - "{indent}> {} {}{}", + "{indent}> {} {}{}{}", step.keyword, step.value, + step.docstring + .as_ref() + .and_then(|doc| cli.verbose.then( + || format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ) + )) + .unwrap_or_default(), step.table .as_ref() .map(|t| format_table(t, self.indent)) @@ -524,6 +604,7 @@ impl Basic { &mut self, step: &gherkin::Step, captures: &CaptureLocations, + cli: CLI, ) { self.clear_last_lines_if_term_present(); @@ -535,6 +616,18 @@ impl Basic { |v| self.styles.ok(v), |v| self.styles.ok(self.styles.bold(v)), ); + let doc_str = self.styles.ok(step + .docstring + .as_ref() + .and_then(|doc| { + cli.verbose.then(|| { + format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ) + }) + }) + .unwrap_or_default()); let step_table = self.styles.ok(step .table .as_ref() @@ -542,9 +635,10 @@ impl Basic { .unwrap_or_default()); self.write_line(&self.styles.ok(format!( - "{indent}{} {}{}", + "{indent}{} {}{}{}", step_keyword, step_value, + doc_str, step_table, indent = " ".repeat(self.indent.saturating_sub(3)), ))) @@ -560,13 +654,21 @@ impl Basic { &mut self, feat: &gherkin::Feature, step: &gherkin::Step, + cli: CLI, ) { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.skipped(format!( - "{indent}?> {} {}{}\n\ + "{indent}?> {} {}{}{}\n\ {indent} Background step failed: {}:{}:{}", step.keyword, step.value, + step.docstring + .as_ref() + .and_then(|doc| cli.verbose.then(|| format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ))) + .unwrap_or_default(), step.table .as_ref() .map(|t| format_table(t, self.indent)) @@ -594,11 +696,12 @@ impl Basic { captures: Option<&CaptureLocations>, world: Option<&W>, err: &event::StepError, + cli: CLI, ) { self.clear_last_lines_if_term_present(); let step_keyword = self.styles.err(format!( - "{indent}\u{2718}> {}", + "{indent}\u{2718}> {}{}", step.keyword, indent = " ".repeat(self.indent.saturating_sub(3)), )); @@ -616,9 +719,16 @@ impl Basic { ); let diagnostics = self.styles.err(format!( - "{}\n\ + "{}{}\n\ {indent} Step failed: {}:{}:{}\n\ {indent} Captured output: {}{}", + step.docstring + .as_ref() + .and_then(|doc| cli.verbose.then(|| format_str_with_indent( + doc.as_str(), + self.indent.saturating_sub(3) + 3, + ))) + .unwrap_or_default(), step.table .as_ref() .map(|t| format_table(t, self.indent)) @@ -629,8 +739,14 @@ impl Basic { .unwrap_or(&feat.name), step.position.line, step.position.col, - format_display_with_indent(err, self.indent.saturating_sub(3) + 3), - format_debug_with_indent(world, self.indent.saturating_sub(3) + 3), + format_str_with_indent( + format!("{}", err).as_str(), + self.indent.saturating_sub(3) + 3 + ), + format_str_with_indent( + world.map(|w| format!("{:#?}", w)).as_deref(), + self.indent.saturating_sub(3) + 3, + ), indent = " ".repeat(self.indent.saturating_sub(3)) )); @@ -656,41 +772,20 @@ pub(crate) fn coerce_error(err: &Info) -> Cow<'static, str> { } } -/// Formats the given [`Debug`] implementor, then adds `indent`s to each line to -/// prettify the output. -fn format_debug_with_indent<'d, D, I>(debug: I, indent: usize) -> String -where - D: Debug + 'd, - I: Into>, -{ - let debug = debug - .into() - .map(|debug| format!("{:#?}", debug)) - .unwrap_or_default() - .lines() - .map(|line| format!("{}{}", " ".repeat(indent), line)) - .join("\n"); - (!debug.is_empty()) - .then(|| format!("\n{}", debug)) - .unwrap_or_default() -} - -/// Formats the given [`Display`] implementor, then adds `indent`s to each line -/// to prettify the output. -fn format_display_with_indent<'d, D, I>(display: I, indent: usize) -> String +/// Formats the given [`str`] by adding `indent`s to each line to prettify +/// the output. +fn format_str_with_indent<'s, I>(str: I, indent: usize) -> String where - D: Display + 'd, - I: Into>, + I: Into>, { - let display = display + let str = str .into() - .map(|display| format!("{}", display)) .unwrap_or_default() .lines() .map(|line| format!("{}{}", " ".repeat(indent), line)) .join("\n"); - (!display.is_empty()) - .then(|| format!("\n{}", display)) + (!str.is_empty()) + .then(|| format!("\n{}", str)) .unwrap_or_default() } diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index 23ccc4d0..59a7f509 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -59,7 +59,13 @@ where ) -> bool, Wr: for<'val> ArbitraryWriter<'val, W, String>, { - async fn handle_event(&mut self, ev: parser::Result>) { + type CLI = Wr::CLI; + + async fn handle_event( + &mut self, + ev: parser::Result>, + cli: &Self::CLI, + ) { use event::{ Cucumber, Feature, Rule, Scenario, Step, StepError::Panic, }; @@ -89,7 +95,7 @@ where _ => ev, }; - self.writer.handle_event(ev).await; + self.writer.handle_event(ev, cli).await; } } diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 1b708cef..76856770 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -21,6 +21,7 @@ pub mod term; use async_trait::async_trait; use sealed::sealed; +use structopt::StructOptInternal; use crate::{event, parser, World}; @@ -41,12 +42,25 @@ pub use self::{ /// [`Cucumber::run_and_exit()`]: crate::Cucumber::run_and_exit #[async_trait(?Send)] pub trait Writer { + /// [`StructOpt`] deriver for CLI options of this [`Writer`]. In case no + /// options present, use [`cli::Empty`]. + /// + /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be + /// merged together, so overlapping arguments will cause runtime panic. + /// + /// [`cli::Empty`]: crate::cli::Empty + /// [`Parser`]: crate::Parser + /// [`Runner`]: crate::Runner + /// [`StructOpt`]: structopt::StructOpt + type CLI: StructOptInternal; + /// Handles the given [`Cucumber`] event. /// /// [`Cucumber`]: crate::event::Cucumber async fn handle_event( &mut self, ev: parser::Result>, + cli: &Self::CLI, ); } diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index a7434281..d0a11b0b 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -60,9 +60,12 @@ impl Normalized { #[async_trait(?Send)] impl> Writer for Normalized { + type CLI = Wr::CLI; + async fn handle_event( &mut self, ev: parser::Result>, + cli: &Self::CLI, ) { use event::{Cucumber, Feature, Rule}; @@ -71,13 +74,13 @@ impl> Writer for Normalized { // This is done to avoid panic if this `Writer` happens to be wrapped // inside `writer::Repeat` or similar. if self.queue.finished { - self.writer.handle_event(ev).await; + self.writer.handle_event(ev, cli).await; return; } match ev { res @ (Err(_) | Ok(Cucumber::Started)) => { - self.writer.handle_event(res).await; + self.writer.handle_event(res, cli).await; } Ok(Cucumber::Finished) => self.queue.finished(), Ok(Cucumber::Feature(f, ev)) => match ev { @@ -97,13 +100,13 @@ impl> Writer for Normalized { } while let Some(feature_to_remove) = - self.queue.emit((), &mut self.writer).await + self.queue.emit((), &mut self.writer, cli).await { self.queue.remove(&feature_to_remove); } if self.queue.is_finished() { - self.writer.handle_event(Ok(Cucumber::Finished)).await; + self.writer.handle_event(Ok(Cucumber::Finished), cli).await; } } } @@ -247,6 +250,7 @@ trait Emitter { self, path: Self::EmittedPath, writer: &mut W, + cli: &W::CLI, ) -> Option; } @@ -330,28 +334,31 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { self, _: (), writer: &mut W, + cli: &W::CLI, ) -> Option { if let Some((f, events)) = self.current_item() { if !events.is_started_emitted() { writer - .handle_event(Ok(event::Cucumber::feature_started( - f.clone(), - ))) + .handle_event( + Ok(event::Cucumber::feature_started(f.clone())), + cli, + ) .await; events.started_emitted(); } while let Some(scenario_or_rule_to_remove) = - events.emit(f.clone(), writer).await + events.emit(f.clone(), writer, cli).await { events.remove(&scenario_or_rule_to_remove); } if events.is_finished() { writer - .handle_event(Ok(event::Cucumber::feature_finished( - f.clone(), - ))) + .handle_event( + Ok(event::Cucumber::feature_finished(f.clone())), + cli, + ) .await; return Some(f.clone()); } @@ -469,13 +476,15 @@ impl<'me, World> Emitter for &'me mut FeatureQueue { self, feature: Self::EmittedPath, writer: &mut W, + cli: &W::CLI, ) -> Option { match self.current_item()? { - Either::Left((rule, events)) => { - events.emit((feature, rule), writer).await.map(Either::Left) - } + Either::Left((rule, events)) => events + .emit((feature, rule), writer, cli) + .await + .map(Either::Left), Either::Right((scenario, events)) => events - .emit((feature, None, scenario), writer) + .emit((feature, None, scenario), writer, cli) .await .map(Either::Right), } @@ -504,20 +513,28 @@ impl<'me, World> Emitter for &'me mut RulesQueue { self, (feature, rule): Self::EmittedPath, writer: &mut W, + cli: &W::CLI, ) -> Option { if !self.is_started_emitted() { writer - .handle_event(Ok(event::Cucumber::rule_started( - feature.clone(), - rule.clone(), - ))) + .handle_event( + Ok(event::Cucumber::rule_started( + feature.clone(), + rule.clone(), + )), + cli, + ) .await; self.started_emitted(); } while let Some((scenario, events)) = self.current_item() { if let Some(should_be_removed) = events - .emit((feature.clone(), Some(rule.clone()), scenario), writer) + .emit( + (feature.clone(), Some(rule.clone()), scenario), + writer, + cli, + ) .await { self.remove(&should_be_removed); @@ -528,10 +545,10 @@ impl<'me, World> Emitter for &'me mut RulesQueue { if self.is_finished() { writer - .handle_event(Ok(event::Cucumber::rule_finished( - feature, - rule.clone(), - ))) + .handle_event( + Ok(event::Cucumber::rule_finished(feature, rule.clone())), + cli, + ) .await; return Some(rule); } @@ -571,6 +588,7 @@ impl<'me, World> Emitter for &'me mut ScenariosQueue { self, (feature, rule, scenario): Self::EmittedPath, writer: &mut W, + cli: &W::CLI, ) -> Option { while let Some(ev) = self.current_item() { let should_be_removed = matches!(ev, event::Scenario::Finished); @@ -581,7 +599,7 @@ impl<'me, World> Emitter for &'me mut ScenariosQueue { scenario.clone(), ev, ); - writer.handle_event(Ok(ev)).await; + writer.handle_event(Ok(ev), cli).await; if should_be_removed { return Some(scenario.clone()); diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 23b4916a..8d5046f7 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -49,18 +49,24 @@ where Wr: Writer, F: Fn(&parser::Result>) -> bool, { - async fn handle_event(&mut self, ev: parser::Result>) { + type CLI = Wr::CLI; + + async fn handle_event( + &mut self, + ev: parser::Result>, + cli: &Self::CLI, + ) { if (self.filter)(&ev) { self.events.push(ev.clone()); } let is_finished = matches!(ev, Ok(event::Cucumber::Finished)); - self.writer.handle_event(ev).await; + self.writer.handle_event(ev, cli).await; if is_finished { for ev in mem::take(&mut self.events) { - self.writer.handle_event(ev).await; + self.writer.handle_event(ev, cli).await; } } } diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index d886f7b2..bcb1eb5f 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -141,7 +141,13 @@ where W: World, Wr: for<'val> ArbitraryWriter<'val, W, String>, { - async fn handle_event(&mut self, ev: parser::Result>) { + type CLI = Wr::CLI; + + async fn handle_event( + &mut self, + ev: parser::Result>, + cli: &Self::CLI, + ) { use event::{Cucumber, Feature, Rule}; let mut finished = false; @@ -162,7 +168,7 @@ where Ok(Cucumber::Started) => {} }; - self.writer.handle_event(ev).await; + self.writer.handle_event(ev, cli).await; if finished { self.writer.write(Styles::new().summary(self)).await; diff --git a/tests/features/output/ambiguous_step.feature.out b/tests/features/output/ambiguous_step.feature.out index e3727a3f..8e9510b5 100644 --- a/tests/features/output/ambiguous_step.feature.out +++ b/tests/features/output/ambiguous_step.feature.out @@ -2,7 +2,7 @@ Started Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Started) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Started)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Started))) -Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 12, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 20, column: 1 }))] }))))) +Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 14, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 22, column: 1 }))] }))))) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Finished)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Finished) Finished diff --git a/tests/features/wait/outline.feature b/tests/features/wait/outline.feature index 23606ee6..6c022aa0 100644 --- a/tests/features/wait/outline.feature +++ b/tests/features/wait/outline.feature @@ -4,6 +4,9 @@ Feature: Outline Given secs When secs Then secs + """ + Doc String + """ Examples: | wait | diff --git a/tests/output.rs b/tests/output.rs index 4a3a7c06..7e7d1a06 100644 --- a/tests/output.rs +++ b/tests/output.rs @@ -1,7 +1,9 @@ use std::{borrow::Cow, cmp::Ordering, convert::Infallible, fmt::Debug}; use async_trait::async_trait; -use cucumber::{event, given, parser, step, then, when, WorldInit, Writer}; +use cucumber::{ + cli, event, given, parser, step, then, when, WorldInit, Writer, +}; use itertools::Itertools as _; use once_cell::sync::Lazy; use regex::Regex; @@ -34,9 +36,12 @@ struct DebugWriter(String); #[async_trait(?Send)] impl Writer for DebugWriter { + type CLI = cli::Empty; + async fn handle_event( &mut self, ev: parser::Result>, + _: &Self::CLI, ) { use event::{Cucumber, Feature, Rule, Scenario, Step, StepError}; From a4a8f62104d1af1d05847074d16336e492228873 Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 20 Oct 2021 12:27:28 +0300 Subject: [PATCH 02/27] Add --scenario-name and --scenario-tags CLI options --- src/cli.rs | 25 +++++++++++++-- src/cucumber.rs | 33 +++++++++++--------- src/lib.rs | 1 + src/runner/basic.rs | 19 +++++++++--- src/tag.rs | 33 ++++++++++++++++++++ src/writer/basic.rs | 48 ++++++++++++++++++++++++++++- tests/features/wait/outline.feature | 1 + 7 files changed, 137 insertions(+), 23 deletions(-) create mode 100644 src/tag.rs diff --git a/src/cli.rs b/src/cli.rs index 767e6595..2d4ff583 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,7 @@ //! CLI options. +use gherkin::tagexpr::TagOperation; use regex::Regex; use structopt::StructOpt; @@ -22,8 +23,23 @@ where Writer: StructOpt, { /// Regex to select scenarios from. - #[structopt(short = "e", long = "expression", name = "regex")] - pub filter: Option, + #[structopt( + short = "n", + long = "name", + name = "regex", + visible_alias = "scenario-name" + )] + pub re_filter: Option, + + /// Regex to select scenarios from. + #[structopt( + short = "t", + long = "tags", + name = "tagexpr", + visible_alias = "scenario-tags", + conflicts_with = "regex" + )] + pub tags_filter: Option, /// [`Parser`] CLI options. /// @@ -44,7 +60,10 @@ where pub writer: Writer, } -/// Empty CLI options. +// Workaround overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr(doc, doc = "Empty CLI options.")] #[derive(StructOpt, Clone, Copy, Debug)] pub struct Empty { /// This field exists only because [`StructOpt`] derive macro doesn't diff --git a/src/cucumber.rs b/src/cucumber.rs index 133c29f9..19f67a1b 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -25,8 +25,9 @@ use regex::Regex; use structopt::StructOpt as _; use crate::{ - cli, event, parser, runner, step, writer, ArbitraryWriter, FailureWriter, - Parser, Runner, ScenarioType, Step, World, Writer, WriterExt as _, + cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, + FailureWriter, Parser, Runner, ScenarioType, Step, World, Writer, + WriterExt as _, }; /// Top-level [Cucumber] executor. @@ -682,23 +683,25 @@ where ) -> bool + 'static, { - let (filter_cli, parser_cli, runner_cli, writer_cli) = { - let cli::Opts { - filter, - parser, - runner, - writer, - } = cli::Opts::::from_args(); - - (filter, parser, runner, writer) - }; + let cli::Opts { + re_filter, + tags_filter, + parser: parser_cli, + runner: runner_cli, + writer: writer_cli, + } = cli::Opts::::from_args(); let filter = move |f: &gherkin::Feature, r: Option<&gherkin::Rule>, s: &gherkin::Scenario| { - filter_cli - .as_ref() - .map_or_else(|| filter(f, r, s), |f| f.is_match(&s.name)) + re_filter.as_ref().map_or_else( + || { + tags_filter + .as_ref() + .map_or_else(|| filter(f, r, s), |f| f.eval(&s.tags)) + }, + |f| f.is_match(&s.name), + ) }; let Cucumber { diff --git a/src/lib.rs b/src/lib.rs index d2e1a984..1cc5d3dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,7 @@ pub mod feature; pub mod parser; pub mod runner; pub mod step; +pub mod tag; pub mod writer; #[cfg(feature = "macros")] diff --git a/src/runner/basic.rs b/src/runner/basic.rs index fa4b643f..35de8ed5 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -34,9 +34,9 @@ use futures::{ }; use itertools::Itertools as _; use regex::{CaptureLocations, Regex}; +use structopt::StructOpt; use crate::{ - cli, event::{self, HookType}, feature::Ext as _, parser, step, Runner, Step, World, @@ -142,6 +142,17 @@ pub struct Basic< after_hook: Option, } +// Workaround overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr(doc, doc = "CLI options of [`Basic`].")] +#[derive(Clone, Copy, Debug, StructOpt)] +pub struct CLI { + /// Number of concurrent scenarios. + #[structopt(long, name = "int")] + pub concurrent: Option, +} + // Implemented manually to omit redundant trait bounds on `World` and to omit // outputting `F`. impl fmt::Debug for Basic { @@ -368,12 +379,12 @@ where ) -> LocalBoxFuture<'a, ()> + 'static, { - type CLI = cli::Empty; + type CLI = CLI; type EventStream = LocalBoxStream<'static, parser::Result>>; - fn run(self, features: S, _: cli::Empty) -> Self::EventStream + fn run(self, features: S, cli: CLI) -> Self::EventStream where S: Stream> + 'static, { @@ -396,7 +407,7 @@ where ); let execute = execute( buffer, - max_concurrent_scenarios, + cli.concurrent.or(max_concurrent_scenarios), steps, sender, before_hook, diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 00000000..26b8a307 --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2018-2021 Brendan Molloy , +// Ilya Solovyiov , +// Kai Ren +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! [`TagOperation`] extension. + +use gherkin::tagexpr::TagOperation; +use sealed::sealed; + +/// Helper method to evaluate [`TagOperation`]. +#[sealed] +pub trait Ext { + /// Evaluates [`TagOperation`]. + fn eval(&self, tags: &[String]) -> bool; +} + +#[sealed] +impl Ext for TagOperation { + fn eval(&self, tags: &[String]) -> bool { + match self { + TagOperation::And(l, r) => l.eval(tags) & r.eval(tags), + TagOperation::Or(l, r) => l.eval(tags) | r.eval(tags), + TagOperation::Not(tag) => !tag.eval(tags), + TagOperation::Tag(t) => tags.iter().any(|tag| tag == t), + } + } +} diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 809c09ab..496d935a 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -15,6 +15,7 @@ use std::{ cmp, fmt::{Debug, Display}, ops::Deref, + str::FromStr, }; use async_trait::async_trait; @@ -50,12 +51,51 @@ pub struct Basic { lines_to_clear: usize, } -/// CLI options of [`Basic`]. +// Workaround overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr(doc, doc = "CLI options of [`Basic`].")] #[derive(Clone, Copy, Debug, StructOpt)] pub struct CLI { /// Outputs Doc String, if present. #[structopt(long)] pub verbose: bool, + + /// Indicates, whether output should be colored or not. + #[structopt( + long, + short, + name = "auto|enabled|disabled", + default_value = "auto" + )] + pub colors: Colors, +} + +/// Indicates, whether output should be colored or not. +#[derive(Clone, Copy, Debug)] +pub enum Colors { + /// Lets [`console::colors_enabled()`] to decide, whether output should be + /// colored or not. + Auto, + + /// Forces colored output. + Enabled, + + /// Forces basic output. + Disabled, +} + +impl FromStr for Colors { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "auto" => Ok(Self::Auto), + "enabled" => Ok(Self::Enabled), + "disabled" => Ok(Self::Disabled), + _ => Err("possible options: auto, enabled, disabled"), + } + } } #[async_trait(?Send)] @@ -70,6 +110,12 @@ impl Writer for Basic { ) { use event::{Cucumber, Feature}; + match cli.colors { + Colors::Enabled => self.styles.is_present = true, + Colors::Disabled => self.styles.is_present = false, + Colors::Auto => {} + }; + match ev { Err(err) => self.parsing_failed(&err), Ok(Cucumber::Started | Cucumber::Finished) => {} diff --git a/tests/features/wait/outline.feature b/tests/features/wait/outline.feature index 6c022aa0..67e43564 100644 --- a/tests/features/wait/outline.feature +++ b/tests/features/wait/outline.feature @@ -1,5 +1,6 @@ Feature: Outline + @tag Scenario Outline: wait Given secs When secs From f0220bbb3771a74244a8f0b36eef2dc3199d35dd Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 20 Oct 2021 13:36:40 +0300 Subject: [PATCH 03/27] Add --features option --- src/cli.rs | 2 +- src/parser/basic.rs | 136 +++++++++++++++++++++++++++++--------------- src/runner/basic.rs | 2 +- src/writer/basic.rs | 4 +- 4 files changed, 93 insertions(+), 51 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2d4ff583..cfd18453 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -60,7 +60,7 @@ where pub writer: Writer, } -// Workaround overwritten doc-comments. +// Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr(doc, doc = "Empty CLI options.")] diff --git a/src/parser/basic.rs b/src/parser/basic.rs index b7138896..33eb9d94 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -13,6 +13,7 @@ use std::{ borrow::Cow, path::{Path, PathBuf}, + str::FromStr, sync::Arc, vec, }; @@ -20,9 +21,10 @@ use std::{ use derive_more::{Display, Error}; use futures::stream; use gherkin::GherkinEnv; -use globwalk::GlobWalkerBuilder; +use globwalk::{GlobWalker, GlobWalkerBuilder}; +use structopt::StructOpt; -use crate::{cli, feature::Ext as _}; +use crate::feature::Ext as _; use super::{Error as ParseError, Parser}; @@ -38,59 +40,99 @@ pub struct Basic { language: Option>, } +// Workaround for overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr(doc, doc = "CLI options of [`Basic`].")] +#[allow(missing_debug_implementations)] +#[derive(StructOpt)] +pub struct CLI { + /// Feature-files glob pattern. + #[structopt(long, short, name = "glob")] + pub features: Option, +} + +/// [`GlobWalker`] wrapper with [`FromStr`] impl. +#[allow(missing_debug_implementations)] +pub struct Walker(GlobWalker); + +impl FromStr for Walker { + type Err = globwalk::GlobError; + + fn from_str(s: &str) -> Result { + globwalk::glob(s).map(Walker) + } +} + impl> Parser for Basic { - type CLI = cli::Empty; + type CLI = CLI; type Output = stream::Iter>>; - fn parse(self, path: I, _: cli::Empty) -> Self::Output { - let features = || { + fn parse(self, path: I, cli: Self::CLI) -> Self::Output { + let walk = |walker: GlobWalker| { + walker + .filter_map(Result::ok) + .filter(|entry| { + entry + .path() + .extension() + .map(|ext| ext == "feature") + .unwrap_or_default() + }) + .map(|entry| { + let env = self + .language + .as_ref() + .and_then(|l| GherkinEnv::new(l).ok()) + .unwrap_or_default(); + gherkin::Feature::parse_path(entry.path(), env) + }) + .collect::>() + }; + + let get_path = || { let path = path.as_ref(); - let path = match path.canonicalize().or_else(|_| { - let mut buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - buf.push( - path.strip_prefix("/") - .or_else(|_| path.strip_prefix("./")) - .unwrap_or(path), - ); - buf.as_path().canonicalize() - }) { - Ok(p) => p, - Err(err) => { - return vec![ - (Err(Arc::new(gherkin::ParseFileError::Reading { - path: path.to_path_buf(), - source: err, - }) - .into())), - ]; - } - }; + path.canonicalize() + .or_else(|_| { + let mut buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + buf.push( + path.strip_prefix("/") + .or_else(|_| path.strip_prefix("./")) + .unwrap_or(path), + ); + buf.as_path().canonicalize() + }) + .map_err(|e| gherkin::ParseFileError::Reading { + path: path.to_path_buf(), + source: e, + }) + }; - let features = if path.is_file() { - let env = self - .language - .as_ref() - .and_then(|l| GherkinEnv::new(l).ok()) - .unwrap_or_default(); - vec![gherkin::Feature::parse_path(path, env)] + let features = || { + let features = if let Some(walker) = cli.features { + walk(walker.0) } else { - let walker = GlobWalkerBuilder::new(path, "*.feature") - .case_insensitive(true) - .build() - .unwrap(); - walker - .filter_map(Result::ok) - .map(|entry| { - let env = self - .language - .as_ref() - .and_then(|l| GherkinEnv::new(l).ok()) - .unwrap_or_default(); - gherkin::Feature::parse_path(entry.path(), env) - }) - .collect::>() + let path = match get_path() { + Ok(path) => path, + Err(e) => return vec![Err(Arc::new(e).into())], + }; + + if path.is_file() { + let env = self + .language + .as_ref() + .and_then(|l| GherkinEnv::new(l).ok()) + .unwrap_or_default(); + vec![gherkin::Feature::parse_path(path, env)] + } else { + let walker = GlobWalkerBuilder::new(path, "*.feature") + .case_insensitive(true) + .build() + .unwrap(); + walk(walker) + } }; features diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 35de8ed5..12fc8802 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -142,7 +142,7 @@ pub struct Basic< after_hook: Option, } -// Workaround overwritten doc-comments. +// Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr(doc, doc = "CLI options of [`Basic`].")] diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 496d935a..17846e53 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -51,13 +51,13 @@ pub struct Basic { lines_to_clear: usize, } -// Workaround overwritten doc-comments. +// Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr(doc, doc = "CLI options of [`Basic`].")] #[derive(Clone, Copy, Debug, StructOpt)] pub struct CLI { - /// Outputs Doc String, if present. + /// Outputs Step's Doc String, if present. #[structopt(long)] pub verbose: bool, From 696de67fe41dd59bf399ebbfe57435cebdb3c2f1 Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 20 Oct 2021 13:53:04 +0300 Subject: [PATCH 04/27] Correction --- src/writer/basic.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 17846e53..15ee659d 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -65,7 +65,7 @@ pub struct CLI { #[structopt( long, short, - name = "auto|enabled|disabled", + name = "auto|always|never", default_value = "auto" )] pub colors: Colors, @@ -79,10 +79,10 @@ pub enum Colors { Auto, /// Forces colored output. - Enabled, + Always, /// Forces basic output. - Disabled, + Never, } impl FromStr for Colors { @@ -91,9 +91,9 @@ impl FromStr for Colors { fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "auto" => Ok(Self::Auto), - "enabled" => Ok(Self::Enabled), - "disabled" => Ok(Self::Disabled), - _ => Err("possible options: auto, enabled, disabled"), + "always" => Ok(Self::Always), + "never" => Ok(Self::Never), + _ => Err("possible options: auto, always, never"), } } } @@ -111,8 +111,8 @@ impl Writer for Basic { use event::{Cucumber, Feature}; match cli.colors { - Colors::Enabled => self.styles.is_present = true, - Colors::Disabled => self.styles.is_present = false, + Colors::Always => self.styles.is_present = true, + Colors::Never => self.styles.is_present = false, Colors::Auto => {} }; From 7b244072a6b6fbdd7c6faaf5af96b15784d7e5a7 Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 20 Oct 2021 16:19:07 +0300 Subject: [PATCH 05/27] Add Cucumber methods for additional user CLI --- src/cli.rs | 23 +++- src/cucumber.rs | 257 ++++++++++++++++++++++++++++++++++++++------ src/writer/basic.rs | 2 +- src/writer/mod.rs | 41 +++++++ 4 files changed, 286 insertions(+), 37 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index cfd18453..8a20d967 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,7 +15,7 @@ use regex::Regex; use structopt::StructOpt; /// Run the tests, pet a dog!. -#[derive(StructOpt, Debug)] +#[derive(Debug, StructOpt)] pub struct Opts where Parser: StructOpt, @@ -64,10 +64,29 @@ where // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr(doc, doc = "Empty CLI options.")] -#[derive(StructOpt, Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, StructOpt)] pub struct Empty { /// This field exists only because [`StructOpt`] derive macro doesn't /// support unit structs. #[structopt(skip)] skipped: (), } + +// Workaround for overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr(doc, doc = "Composes two [`StructOpt`] derivers together.")] +#[derive(Debug, StructOpt)] +pub struct Compose +where + L: StructOpt, + R: StructOpt, +{ + /// [`StructOpt`] deriver. + #[structopt(flatten)] + pub left: L, + + /// [`StructOpt`] deriver. + #[structopt(flatten)] + pub right: R, +} diff --git a/src/cucumber.rs b/src/cucumber.rs index 19f67a1b..e0d482c4 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -20,9 +20,9 @@ use std::{ path::Path, }; -use futures::{future::LocalBoxFuture, StreamExt as _}; +use futures::{future::LocalBoxFuture, Future, StreamExt as _}; use regex::Regex; -use structopt::StructOpt as _; +use structopt::{StructOpt, StructOptInternal}; use crate::{ cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, @@ -673,8 +673,110 @@ where /// /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario - #[allow(clippy::non_ascii_literal)] pub async fn filter_run(self, input: I, filter: F) -> Wr + where + F: Fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool + + 'static, + { + self.filter_run_with_cli( + input, + filter, + cli::Opts::::from_args(), + ) + .await + } + + /// Runs [`Cucumber`] with [`Scenario`]s filter and additional CLI options. + /// + /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which + /// produces events handled by a [`Writer`]. + /// + /// # Example + /// + /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag: + /// ```rust + /// # use std::convert::Infallible; + /// # + /// # use async_trait::async_trait; + /// # use cucumber::{WorldInit, cli}; + /// # + /// # #[derive(Debug, WorldInit)] + /// # struct MyWorld; + /// # + /// # #[async_trait(?Send)] + /// # impl cucumber::World for MyWorld { + /// # type Error = Infallible; + /// # + /// # async fn new() -> Result { + /// # Ok(Self) + /// # } + /// # } + /// # + /// # let fut = async { + /// let (_cli, cucumber) = MyWorld::cucumber() + /// .filter_run_with_additional_cli::( + /// "tests/features/readme", + /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), + /// ); + /// cucumber.await; + /// # }; + /// # + /// # futures::executor::block_on(fut); + /// ``` + /// ```gherkin + /// Feature: Animal feature + /// + /// @cat + /// Scenario: If we feed a hungry cat it will no longer be hungry + /// Given a hungry cat + /// When I feed the cat + /// Then the cat is not hungry + /// + /// @dog + /// Scenario: If we feed a satiated dog it will not become hungry + /// Given a satiated dog + /// When I feed the dog + /// Then the dog is not hungry + /// ``` + /// + /// + /// [`Feature`]: gherkin::Feature + /// [`Scenario`]: gherkin::Scenario + pub fn filter_run_with_additional_cli( + self, + input: I, + filter: impl Fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool + + 'static, + ) -> (CustomCli, impl Future) + where + CustomCli: StructOptInternal, + { + let cli::Compose { left, right } = cli::Compose::< + CustomCli, + cli::Opts, + >::from_args(); + (left, self.filter_run_with_cli(input, filter, right)) + } + + /// Runs [`Cucumber`] with [`Scenario`]s filter with provided CLI options. + async fn filter_run_with_cli( + self, + input: I, + filter: F, + cli: cli::Opts, + ) -> Wr where F: Fn( &gherkin::Feature, @@ -689,7 +791,7 @@ where parser: parser_cli, runner: runner_cli, writer: writer_cli, - } = cli::Opts::::from_args(); + } = cli; let filter = move |f: &gherkin::Feature, r: Option<&gherkin::Rule>, @@ -1014,6 +1116,29 @@ where self.filter_run_and_exit(input, |_, _, _| true).await; } + /// Runs [`Cucumber`]. + /// + /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which + /// produces events handled by a [`Writer`]. + /// + /// # Panics + /// + /// Returned [`Future`] panics if encountered errors while parsing + /// [`Feature`]s or at least one [`Step`] [`Failed`]. + /// + /// [`Failed`]: crate::event::Step::Failed + /// [`Feature`]: gherkin::Feature + /// [`Step`]: gherkin::Step + pub fn run_and_exit_with_additional_cli( + self, + input: I, + ) -> (CustomCli, impl Future) + where + CustomCli: StructOptInternal, + { + self.filter_run_and_exit_with_additional_cli(input, |_, _, _| true) + } + /// Runs [`Cucumber`] with [`Scenario`]s filter. /// /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which @@ -1089,38 +1214,102 @@ where ) -> 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!( - "{} step{} failed", - failed_steps, - (failed_steps > 1).then(|| "s").unwrap_or_default(), - )); - } + self.filter_run(input, filter) + .await + .panic_with_diagnostic_message(); + } - let parsing_errors = writer.parsing_errors(); - if parsing_errors > 0 { - msg.push(format!( - "{} parsing error{}", - parsing_errors, - (parsing_errors > 1).then(|| "s").unwrap_or_default(), - )); - } + /// Runs [`Cucumber`] with [`Scenario`]s filter and additional CLI options. + /// + /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which + /// produces events handled by a [`Writer`]. + /// + /// # Panics + /// + /// Returned [`Future`] panics if encountered errors while parsing + /// [`Feature`]s or at least one [`Step`] [`Failed`]. + /// + /// # Example + /// + /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag: + /// ```rust + /// # use std::convert::Infallible; + /// # + /// # use async_trait::async_trait; + /// # use cucumber::{WorldInit, cli}; + /// # + /// # #[derive(Debug, WorldInit)] + /// # struct MyWorld; + /// # + /// # #[async_trait(?Send)] + /// # impl cucumber::World for MyWorld { + /// # type Error = Infallible; + /// # + /// # async fn new() -> Result { + /// # Ok(Self) + /// # } + /// # } + /// # + /// # let fut = async { + /// let (_cli, cucumber) = MyWorld::cucumber() + /// .filter_run_and_exit_with_additional_cli::( + /// "tests/features/readme", + /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), + /// ); + /// cucumber.await; + /// # }; + /// # + /// # futures::executor::block_on(fut); + /// ``` + /// ```gherkin + /// Feature: Animal feature + /// + /// @cat + /// Scenario: If we feed a hungry cat it will no longer be hungry + /// Given a hungry cat + /// When I feed the cat + /// Then the cat is not hungry + /// + /// @dog + /// Scenario: If we feed a satiated dog it will not become hungry + /// Given a satiated dog + /// When I feed the dog + /// Then the dog is not hungry + /// ``` + /// + /// + /// [`Failed`]: crate::event::Step::Failed + /// [`Feature`]: gherkin::Feature + /// [`Scenario`]: gherkin::Scenario + /// [`Step`]: crate::Step + pub fn filter_run_and_exit_with_additional_cli( + self, + input: I, + filter: impl Fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool + + 'static, + ) -> (CustomCli, impl Future) + where + CustomCli: StructOptInternal, + { + let cli::Compose { left, right } = cli::Compose::< + CustomCli, + cli::Opts, + >::from_args(); - let hook_errors = writer.hook_errors(); - if hook_errors > 0 { - msg.push(format!( - "{} hook error{}", - hook_errors, - (hook_errors > 1).then(|| "s").unwrap_or_default(), - )); - } + let f = async { + self.filter_run_with_cli(input, filter, right) + .await + .panic_with_diagnostic_message(); + }; - panic!("{}", msg.join(", ")); - } + (left, f) } } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 15ee659d..aed4a690 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -468,7 +468,7 @@ impl Basic { ) { self.clear_last_lines_if_term_present(); self.write_line(&self.styles.skipped(format!( - "{indent}? {} {}{}{}{}\n\ + "{indent}? {} {}{}{}\n\ {indent} Step skipped: {}:{}:{}", step.keyword, step.value, diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 76856770..37d36648 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -103,6 +103,47 @@ pub trait Failure: Writer { /// [`Scenario`]: gherkin::Scenario #[must_use] fn hook_errors(&self) -> usize; + + /// Panics with diagnostic message in case [`execution_has_failed`][1]. + /// + /// Default message looks like: + /// `1 step failed, 2 parsing errors, 3 hook errors`. + /// + /// [1]: Self::execution_has_failed() + fn panic_with_diagnostic_message(&self) { + if self.execution_has_failed() { + let mut msg = Vec::with_capacity(3); + + let failed_steps = self.failed_steps(); + if failed_steps > 0 { + msg.push(format!( + "{} step{} failed", + failed_steps, + (failed_steps > 1).then(|| "s").unwrap_or_default(), + )); + } + + let parsing_errors = self.parsing_errors(); + if parsing_errors > 0 { + msg.push(format!( + "{} parsing error{}", + parsing_errors, + (parsing_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + let hook_errors = self.hook_errors(); + if hook_errors > 0 { + msg.push(format!( + "{} hook error{}", + hook_errors, + (hook_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + panic!("{}", msg.join(", ")); + } + } } /// Extension of [`Writer`] allowing its normalization and summarization. From d9c2589ccba97661681ba188b5b22b7089755f79 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 08:09:58 +0300 Subject: [PATCH 06/27] Correction --- src/cucumber.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index e0d482c4..37a6f596 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -718,7 +718,7 @@ where /// # /// # let fut = async { /// let (_cli, cucumber) = MyWorld::cucumber() - /// .filter_run_with_additional_cli::( + /// .filter_run_with_additional_cli::( /// "tests/features/readme", /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), /// ); @@ -750,18 +750,19 @@ where /// /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario - pub fn filter_run_with_additional_cli( + pub fn filter_run_with_additional_cli( self, input: I, - filter: impl Fn( + filter: F, + ) -> (CustomCli, impl Future) + where + CustomCli: StructOptInternal, + F: Fn( &gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario, ) -> bool + 'static, - ) -> (CustomCli, impl Future) - where - CustomCli: StructOptInternal, { let cli::Compose { left, right } = cli::Compose::< CustomCli, @@ -771,6 +772,8 @@ where } /// Runs [`Cucumber`] with [`Scenario`]s filter with provided CLI options. + /// + /// [`Scenario`]: gherkin::Scenario async fn filter_run_with_cli( self, input: I, @@ -1252,7 +1255,7 @@ where /// # /// # let fut = async { /// let (_cli, cucumber) = MyWorld::cucumber() - /// .filter_run_and_exit_with_additional_cli::( + /// .filter_run_and_exit_with_additional_cli::( /// "tests/features/readme", /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), /// ); @@ -1286,18 +1289,19 @@ where /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario /// [`Step`]: crate::Step - pub fn filter_run_and_exit_with_additional_cli( + pub fn filter_run_and_exit_with_additional_cli( self, input: I, - filter: impl Fn( + filter: F, + ) -> (CustomCli, impl Future) + where + CustomCli: StructOptInternal, + F: Fn( &gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario, ) -> bool + 'static, - ) -> (CustomCli, impl Future) - where - CustomCli: StructOptInternal, { let cli::Compose { left, right } = cli::Compose::< CustomCli, From 0b580591ae724e86c7cb53e43eb481092a047af8 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 08:44:02 +0300 Subject: [PATCH 07/27] Corrections --- Cargo.toml | 1 - Makefile | 2 +- src/cli.rs | 10 ++++++++-- src/parser/basic.rs | 10 ++++++---- src/runner/basic.rs | 5 ++++- src/writer/basic.rs | 7 +++++-- src/writer/normalized.rs | 6 +++--- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 12c701ae..59c6075c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ macros = ["cucumber-codegen", "inventory"] [dependencies] async-trait = "0.1.40" atty = "0.2.14" -clap = "2.33" console = "0.15" derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false } either = "1.6" diff --git a/Makefile b/Makefile index dae5a41d..44c3d98a 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ cargo.fmt: # make cargo.lint cargo.lint: - cargo clippy --workspace -- -D warnings + cargo +stable clippy --workspace -- -D warnings diff --git a/src/cli.rs b/src/cli.rs index 8a20d967..844aa1e6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -62,7 +62,10 @@ where // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 -#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) +)] #[cfg_attr(doc, doc = "Empty CLI options.")] #[derive(Clone, Copy, Debug, StructOpt)] pub struct Empty { @@ -74,7 +77,10 @@ pub struct Empty { // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 -#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) +)] #[cfg_attr(doc, doc = "Composes two [`StructOpt`] derivers together.")] #[derive(Debug, StructOpt)] pub struct Compose diff --git a/src/parser/basic.rs b/src/parser/basic.rs index e06c33f2..b53e85bb 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -14,7 +14,6 @@ use std::{ borrow::Cow, path::{Path, PathBuf}, str::FromStr, - sync::Arc, vec, }; @@ -42,7 +41,10 @@ pub struct Basic { // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 -#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) +)] #[cfg_attr(doc, doc = "CLI options of [`Basic`].")] #[allow(missing_debug_implementations)] #[derive(StructOpt)] @@ -116,7 +118,7 @@ impl> Parser for Basic { } else { let path = match get_path() { Ok(path) => path, - Err(e) => return vec![Err(Arc::new(e).into())], + Err(e) => return vec![Err(e.into())], }; if path.is_file() { @@ -141,7 +143,7 @@ impl> Parser for Basic { .into_iter() .map(|f| match f { Ok(f) => f.expand_examples().map_err(ParseError::from), - Err(e) => Err(Arc::new(e).into()), + Err(e) => Err(e.into()), }) .collect() }; diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 83b42418..e39c376b 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -144,7 +144,10 @@ pub struct Basic< // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 -#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) +)] #[cfg_attr(doc, doc = "CLI options of [`Basic`].")] #[derive(Clone, Copy, Debug, StructOpt)] pub struct CLI { diff --git a/src/writer/basic.rs b/src/writer/basic.rs index f8a36810..6bc36d73 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -54,7 +54,10 @@ pub struct Basic { // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 -#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) +)] #[cfg_attr(doc, doc = "CLI options of [`Basic`].")] #[derive(Clone, Copy, Debug, StructOpt)] pub struct CLI { @@ -107,7 +110,7 @@ impl Writer for Basic { async fn handle_event( &mut self, ev: parser::Result>, - cli: Self::CLI, + cli: &Self::CLI, ) { use event::{Cucumber, Feature}; diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 91a42d2f..00e33137 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -363,10 +363,10 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { if events.is_finished() { writer - .handle_event(Ok(event::Cucumber::feature_finished( - Arc::clone(&f), + .handle_event( + Ok(event::Cucumber::feature_finished(Arc::clone(&f))), cli, - ))) + ) .await; return Some(Arc::clone(&f)); } From d7bb7247e07be31c8c1d9ed99502f7714aaa6dd5 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 10:05:31 +0300 Subject: [PATCH 08/27] Corrections --- Makefile | 7 ++- src/cli.rs | 12 ++++ src/cucumber.rs | 142 +++++++++----------------------------------- src/writer/basic.rs | 58 +++++++++--------- tests/wait.rs | 8 ++- 5 files changed, 80 insertions(+), 147 deletions(-) diff --git a/Makefile b/Makefile index 44c3d98a..e95a04b6 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ cargo.doc: ifeq ($(clean),yes) @rm -rf target/doc/ endif - cargo doc $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ + cargo +stable doc $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ --all-features \ $(if $(call eq,$(private),no),,--document-private-items) \ $(if $(call eq,$(open),no),,--open) @@ -82,7 +82,8 @@ cargo.lint: # make test.cargo [crate=] test.cargo: - cargo test $(if $(call eq,$(crate),),--workspace,-p $(crate)) --all-features + cargo +stable test $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ + --all-features # Run Rust tests of Book. @@ -91,7 +92,7 @@ test.cargo: # make test.book test.book: - cargo test --manifest-path book/tests/Cargo.toml + cargo +stable test --manifest-path book/tests/Cargo.toml diff --git a/src/cli.rs b/src/cli.rs index 844aa1e6..0871bb14 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -96,3 +96,15 @@ where #[structopt(flatten)] pub right: R, } + +impl Compose +where + L: StructOpt, + R: StructOpt, +{ + /// Unpacks [`Compose`] into underlying `CLI`s. + pub fn unpack(self) -> (L, R) { + let Compose { left, right } = self; + (left, right) + } +} diff --git a/src/cucumber.rs b/src/cucumber.rs index d09cc776..d94be73d 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -20,9 +20,9 @@ use std::{ path::Path, }; -use futures::{future::LocalBoxFuture, Future, StreamExt as _}; +use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; -use structopt::{StructOpt, StructOptInternal}; +use structopt::StructOpt as _; use crate::{ cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, @@ -696,102 +696,21 @@ where + 'static, { self.filter_run_with_cli( + cli::Opts::::from_args(), input, filter, - cli::Opts::::from_args(), ) .await } - /// Runs [`Cucumber`] with [`Scenario`]s filter and additional CLI options. - /// - /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which - /// produces events handled by a [`Writer`]. - /// - /// # Example - /// - /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag: - /// ```rust - /// # use std::convert::Infallible; - /// # - /// # use async_trait::async_trait; - /// # use cucumber::{WorldInit, cli}; - /// # - /// # #[derive(Debug, WorldInit)] - /// # struct MyWorld; - /// # - /// # #[async_trait(?Send)] - /// # impl cucumber::World for MyWorld { - /// # type Error = Infallible; - /// # - /// # async fn new() -> Result { - /// # Ok(Self) - /// # } - /// # } - /// # - /// # let fut = async { - /// let (_cli, cucumber) = MyWorld::cucumber() - /// .filter_run_with_additional_cli::( - /// "tests/features/readme", - /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), - /// ); - /// cucumber.await; - /// # }; - /// # - /// # futures::executor::block_on(fut); - /// ``` - /// ```gherkin - /// Feature: Animal feature - /// - /// @cat - /// Scenario: If we feed a hungry cat it will no longer be hungry - /// Given a hungry cat - /// When I feed the cat - /// Then the cat is not hungry - /// - /// @dog - /// Scenario: If we feed a satiated dog it will not become hungry - /// Given a satiated dog - /// When I feed the dog - /// Then the dog is not hungry - /// ``` - /// - /// - /// [`Feature`]: gherkin::Feature - /// [`Scenario`]: gherkin::Scenario - pub fn filter_run_with_additional_cli( - self, - input: I, - filter: F, - ) -> (CustomCli, impl Future) - where - CustomCli: StructOptInternal, - F: Fn( - &gherkin::Feature, - Option<&gherkin::Rule>, - &gherkin::Scenario, - ) -> bool - + 'static, - { - let cli::Compose { left, right } = cli::Compose::< - CustomCli, - cli::Opts, - >::from_args(); - (left, self.filter_run_with_cli(input, filter, right)) - } - /// Runs [`Cucumber`] with [`Scenario`]s filter with provided CLI options. /// /// [`Scenario`]: gherkin::Scenario - async fn filter_run_with_cli( + pub async fn filter_run_with_cli( self, + cli: cli::Opts, input: I, filter: F, - cli: cli::Opts, ) -> Wr where F: Fn( @@ -1145,14 +1064,13 @@ where /// [`Failed`]: crate::event::Step::Failed /// [`Feature`]: gherkin::Feature /// [`Step`]: gherkin::Step - pub fn run_and_exit_with_additional_cli( + pub async fn run_and_exit_with_cli( self, + cli: cli::Opts, input: I, - ) -> (CustomCli, impl Future) - where - CustomCli: StructOptInternal, - { - self.filter_run_and_exit_with_additional_cli(input, |_, _, _| true) + ) { + self.filter_run_and_exit_with_cli(cli, input, |_, _, _| true) + .await } /// Runs [`Cucumber`] with [`Scenario`]s filter. @@ -1253,6 +1171,7 @@ where /// # /// # use async_trait::async_trait; /// # use cucumber::{WorldInit, cli}; + /// # use structopt::StructOpt as _; /// # /// # #[derive(Debug, WorldInit)] /// # struct MyWorld; @@ -1267,12 +1186,17 @@ where /// # } /// # /// # let fut = async { - /// let (_cli, cucumber) = MyWorld::cucumber() - /// .filter_run_and_exit_with_additional_cli::( + /// let (_custom, cli) = + /// cli::Compose::>::from_args() + /// .unpack(); + /// + /// MyWorld::cucumber() + /// .filter_run_and_exit_with_cli( + /// cli, /// "tests/features/readme", /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), - /// ); - /// cucumber.await; + /// ) + /// .await; /// # }; /// # /// # futures::executor::block_on(fut); @@ -1302,31 +1226,21 @@ where /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario /// [`Step`]: crate::Step - pub fn filter_run_and_exit_with_additional_cli( + pub async fn filter_run_and_exit_with_cli( self, + cli: cli::Opts, input: I, - filter: F, - ) -> (CustomCli, impl Future) - where - CustomCli: StructOptInternal, - F: Fn( + filter: Filter, + ) where + Filter: Fn( &gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario, ) -> bool + 'static, { - let cli::Compose { left, right } = cli::Compose::< - CustomCli, - cli::Opts, - >::from_args(); - - let f = async { - self.filter_run_with_cli(input, filter, right) - .await - .panic_with_diagnostic_message(); - }; - - (left, f) + self.filter_run_with_cli(cli, input, filter) + .await + .panic_with_diagnostic_message(); } } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 6bc36d73..f7f72b1f 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -317,10 +317,12 @@ impl Basic { sc.position.line, sc.position.col, coerce_error(info), - format_str_with_indent( - world.map(|w| format!("{:#?}", w)).as_deref(), - self.indent.saturating_sub(3) + 3, - ), + world + .map(|w| format_str_with_indent( + format!("{:#?}", w), + self.indent.saturating_sub(3) + 3, + )) + .unwrap_or_default(), indent = " ".repeat(self.indent.saturating_sub(3)), ))) } @@ -404,7 +406,7 @@ impl Basic { .as_ref() .and_then(|doc| cli.verbose.then( || format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ) )) @@ -447,7 +449,7 @@ impl Basic { .and_then(|doc| { cli.verbose.then(|| { format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ) }) @@ -488,7 +490,7 @@ impl Basic { step.docstring .as_ref() .and_then(|doc| cli.verbose.then(|| format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ))) .unwrap_or_default(), @@ -546,7 +548,7 @@ impl Basic { step.docstring .as_ref() .and_then(|doc| cli.verbose.then(|| format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ))) .unwrap_or_default(), @@ -561,13 +563,15 @@ impl Basic { step.position.line, step.position.col, format_str_with_indent( - format!("{}", err).as_str(), - self.indent.saturating_sub(3) + 3 - ), - format_str_with_indent( - world.map(|w| format!("{:#?}", w)).as_deref(), + format!("{}", err), self.indent.saturating_sub(3) + 3 ), + world + .map(|w| format_str_with_indent( + format!("{:#?}", w), + self.indent.saturating_sub(3) + 3, + )) + .unwrap_or_default(), indent = " ".repeat(self.indent.saturating_sub(3)) )); @@ -641,7 +645,7 @@ impl Basic { .as_ref() .and_then(|doc| cli.verbose.then( || format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ) )) @@ -685,7 +689,7 @@ impl Basic { .and_then(|doc| { cli.verbose.then(|| { format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ) }) @@ -727,7 +731,7 @@ impl Basic { step.docstring .as_ref() .and_then(|doc| cli.verbose.then(|| format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ))) .unwrap_or_default(), @@ -786,7 +790,7 @@ impl Basic { step.docstring .as_ref() .and_then(|doc| cli.verbose.then(|| format_str_with_indent( - doc.as_str(), + doc, self.indent.saturating_sub(3) + 3, ))) .unwrap_or_default(), @@ -801,13 +805,15 @@ impl Basic { step.position.line, step.position.col, format_str_with_indent( - format!("{}", err).as_str(), - self.indent.saturating_sub(3) + 3, - ), - format_str_with_indent( - world.map(|w| format!("{:#?}", w)).as_deref(), + format!("{}", err), self.indent.saturating_sub(3) + 3, ), + world + .map(|w| format_str_with_indent( + format!("{:#?}", w), + self.indent.saturating_sub(3) + 3, + )) + .unwrap_or_default(), indent = " ".repeat(self.indent.saturating_sub(3)) )); @@ -834,13 +840,9 @@ pub(crate) fn coerce_error(err: &Info) -> Cow<'static, str> { /// Formats the given [`str`] by adding `indent`s to each line to prettify /// the output. -fn format_str_with_indent<'s, I>(str: I, indent: usize) -> String -where - I: Into>, -{ +fn format_str_with_indent(str: impl AsRef, indent: usize) -> String { let str = str - .into() - .unwrap_or_default() + .as_ref() .lines() .map(|line| format!("{}{}", " ".repeat(indent), line)) .join("\n"); diff --git a/tests/wait.rs b/tests/wait.rs index ec87e380..7cbfd1ee 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -1,12 +1,16 @@ use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; -use cucumber::{given, then, when, WorldInit}; +use cucumber::{cli, given, then, when, WorldInit}; use futures::FutureExt as _; +use structopt::StructOpt as _; use tokio::time; #[tokio::main] async fn main() { + let (_custom, cli) = + cli::Compose::>::from_args().unpack(); + let res = World::cucumber() .before(|_, _, _, w| { async move { @@ -18,7 +22,7 @@ async fn main() { .after(|_, _, _, _| { time::sleep(Duration::from_millis(10)).boxed_local() }) - .run_and_exit("tests/features/wait"); + .run_and_exit_with_cli(cli, "tests/features/wait"); let err = AssertUnwindSafe(res) .catch_unwind() From c48d942b97ca2877f735138020e46e45ee7335ed Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 12:11:44 +0300 Subject: [PATCH 09/27] Corrections and add todo_or_die crate --- Cargo.toml | 1 + src/cli.rs | 4 +- src/cucumber.rs | 96 ++++++++++++++++++++++++++++++++++++++++----- src/parser/basic.rs | 1 + src/runner/basic.rs | 1 + src/writer/basic.rs | 1 + tests/wait.rs | 3 +- 7 files changed, 95 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 59c6075c..1d862c0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ once_cell = { version = "1.8", features = ["parking_lot"] } regex = "1.5" sealed = "0.3" structopt = "0.3.25" +todo-or-die = { version = "0.1.2", features = ["github"] } # "macros" feature dependencies cucumber-codegen = { version = "0.10", path = "./codegen", optional = true } diff --git a/src/cli.rs b/src/cli.rs index 0871bb14..f08bb99d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -60,6 +60,7 @@ where pub writer: Writer, } +todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( @@ -75,6 +76,7 @@ pub struct Empty { skipped: (), } +todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( @@ -103,7 +105,7 @@ where R: StructOpt, { /// Unpacks [`Compose`] into underlying `CLI`s. - pub fn unpack(self) -> (L, R) { + pub fn into_inner(self) -> (L, R) { let Compose { left, right } = self; (left, right) } diff --git a/src/cucumber.rs b/src/cucumber.rs index d94be73d..15186829 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -705,6 +705,45 @@ where /// Runs [`Cucumber`] with [`Scenario`]s filter with provided CLI options. /// + /// This method exists not to hijack console and give users an ability to + /// compose custom `CLI` options with [`cli::Opts`] using [`cli::Compose`]. + /// + /// # Example + /// + /// ```rust + /// # use std::convert::Infallible; + /// # + /// # use async_trait::async_trait; + /// # use cucumber::{cli, WorldInit}; + /// # use structopt::StructOpt as _; + /// # + /// # #[derive(Debug, WorldInit)] + /// # struct MyWorld; + /// # + /// # #[async_trait(?Send)] + /// # impl cucumber::World for MyWorld { + /// # type Error = Infallible; + /// # + /// # async fn new() -> Result { + /// # Ok(Self) + /// # } + /// # } + /// # + /// # let fut = async { + /// let (_custom, cli) = + /// cli::Compose::>::from_args() + /// .into_inner(); + /// + /// MyWorld::cucumber() + /// .filter_run_with_cli(cli, "tests/features/readme", |_, _, sc| { + /// sc.tags.iter().any(|t| t == "cat") + /// }) + /// .await; + /// # }; + /// # + /// # futures::executor::block_on(fut); + /// ``` + /// /// [`Scenario`]: gherkin::Scenario pub async fn filter_run_with_cli( self, @@ -1051,15 +1090,52 @@ where self.filter_run_and_exit(input, |_, _, _| true).await; } - /// Runs [`Cucumber`]. + /// Runs [`Cucumber`] with provided [`cli::Opts`]. /// /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which /// produces events handled by a [`Writer`]. /// + /// This method exists not to hijack console and give users an ability to + /// compose custom `CLI` options with [`cli::Opts`] using [`cli::Compose`]. + /// /// # Panics /// - /// Returned [`Future`] panics if encountered errors while parsing - /// [`Feature`]s or at least one [`Step`] [`Failed`]. + /// If encountered errors while parsing [`Feature`]s or at least one + /// [`Step`] [`Failed`]. + /// + /// # Example + /// + /// ```rust + /// # use std::convert::Infallible; + /// # + /// # use async_trait::async_trait; + /// # use cucumber::{cli, WorldInit}; + /// # use structopt::StructOpt as _; + /// # + /// # #[derive(Debug, WorldInit)] + /// # struct MyWorld; + /// # + /// # #[async_trait(?Send)] + /// # impl cucumber::World for MyWorld { + /// # type Error = Infallible; + /// # + /// # async fn new() -> Result { + /// # Ok(Self) + /// # } + /// # } + /// # + /// # let fut = async { + /// let (_custom, cli) = + /// cli::Compose::>::from_args() + /// .into_inner(); + /// + /// MyWorld::cucumber() + /// .run_and_exit_with_cli(cli, "tests/features/readme") + /// .await; + /// # }; + /// # + /// # futures::executor::block_on(fut); + /// ``` /// /// [`Failed`]: crate::event::Step::Failed /// [`Feature`]: gherkin::Feature @@ -1153,15 +1229,15 @@ where .panic_with_diagnostic_message(); } - /// Runs [`Cucumber`] with [`Scenario`]s filter and additional CLI options. + /// Runs [`Cucumber`] with [`Scenario`]s filter and provided [`cli::Opts`]. /// - /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which - /// produces events handled by a [`Writer`]. + /// This method exists not to hijack console and give users an ability to + /// compose custom `CLI` options with [`cli::Opts`] using [`cli::Compose`]. /// /// # Panics /// - /// Returned [`Future`] panics if encountered errors while parsing - /// [`Feature`]s or at least one [`Step`] [`Failed`]. + /// If encountered errors while parsing [`Feature`]s or at least one + /// [`Step`] [`Failed`]. /// /// # Example /// @@ -1186,9 +1262,9 @@ where /// # } /// # /// # let fut = async { - /// let (_custom, cli) = + /// let (_custom, cli) = /// cli::Compose::>::from_args() - /// .unpack(); + /// .into_inner(); /// /// MyWorld::cucumber() /// .filter_run_and_exit_with_cli( diff --git a/src/parser/basic.rs b/src/parser/basic.rs index b53e85bb..3db08f25 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -39,6 +39,7 @@ pub struct Basic { language: Option>, } +todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( diff --git a/src/runner/basic.rs b/src/runner/basic.rs index e39c376b..59ba728a 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -142,6 +142,7 @@ pub struct Basic< after_hook: Option, } +todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( diff --git a/src/writer/basic.rs b/src/writer/basic.rs index f7f72b1f..0641cb41 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -52,6 +52,7 @@ pub struct Basic { lines_to_clear: usize, } +todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( diff --git a/tests/wait.rs b/tests/wait.rs index 7cbfd1ee..95756ac2 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -9,7 +9,8 @@ use tokio::time; #[tokio::main] async fn main() { let (_custom, cli) = - cli::Compose::>::from_args().unpack(); + cli::Compose::>::from_args() + .into_inner(); let res = World::cucumber() .before(|_, _, _, w| { From 4119545c05352f57e3a370cb37eed96aa896a6dc Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 12:23:05 +0300 Subject: [PATCH 10/27] Corrections --- book/tests/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/tests/Cargo.toml b/book/tests/Cargo.toml index b9a1f3b4..dadbdb86 100644 --- a/book/tests/Cargo.toml +++ b/book/tests/Cargo.toml @@ -15,7 +15,7 @@ async-trait = "0.1" cucumber = { version = "0.10", path = "../.." } futures = "0.3" skeptic = "0.13" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } +tokio = { version = "1", features = ["full"] } [build-dependencies] skeptic = "0.13" From f0c65db98242f61de14d1e656d8a7c1d822f2f07 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 12:24:32 +0300 Subject: [PATCH 11/27] Lint --- src/cucumber.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cucumber.rs b/src/cucumber.rs index 15186829..0d25d94d 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -1146,7 +1146,7 @@ where input: I, ) { self.filter_run_and_exit_with_cli(cli, input, |_, _, _| true) - .await + .await; } /// Runs [`Cucumber`] with [`Scenario`]s filter. From 7c47f3bbdc8f46e86e9ec3babe888c38ddea5db4 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 12:35:17 +0300 Subject: [PATCH 12/27] Try removing todo-or-die crate --- Cargo.toml | 1 - src/cli.rs | 2 -- src/parser/basic.rs | 1 - src/runner/basic.rs | 1 - src/writer/basic.rs | 1 - 5 files changed, 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d862c0e..59c6075c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ once_cell = { version = "1.8", features = ["parking_lot"] } regex = "1.5" sealed = "0.3" structopt = "0.3.25" -todo-or-die = { version = "0.1.2", features = ["github"] } # "macros" feature dependencies cucumber-codegen = { version = "0.10", path = "./codegen", optional = true } diff --git a/src/cli.rs b/src/cli.rs index f08bb99d..ffb2e9a2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -60,7 +60,6 @@ where pub writer: Writer, } -todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( @@ -76,7 +75,6 @@ pub struct Empty { skipped: (), } -todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( diff --git a/src/parser/basic.rs b/src/parser/basic.rs index 3db08f25..b53e85bb 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -39,7 +39,6 @@ pub struct Basic { language: Option>, } -todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 59ba728a..e39c376b 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -142,7 +142,6 @@ pub struct Basic< after_hook: Option, } -todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 0641cb41..f7f72b1f 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -52,7 +52,6 @@ pub struct Basic { lines_to_clear: usize, } -todo_or_die::issue_closed!("TeXitoi", "structopt", 333); // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 #[cfg_attr( From 90cab8872bfd29f4689a4d052b7c8141c53b1e6d Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 13:22:10 +0300 Subject: [PATCH 13/27] Add Custom generic parameter to cli::Opts --- Makefile | 7 +++--- book/tests/Cargo.toml | 2 +- src/cli.rs | 7 +++++- src/cucumber.rs | 50 +++++++++++++++++++++++++------------------ tests/wait.rs | 4 +--- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index e95a04b6..44c3d98a 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ cargo.doc: ifeq ($(clean),yes) @rm -rf target/doc/ endif - cargo +stable doc $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ + cargo doc $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ --all-features \ $(if $(call eq,$(private),no),,--document-private-items) \ $(if $(call eq,$(open),no),,--open) @@ -82,8 +82,7 @@ cargo.lint: # make test.cargo [crate=] test.cargo: - cargo +stable test $(if $(call eq,$(crate),),--workspace,-p $(crate)) \ - --all-features + cargo test $(if $(call eq,$(crate),),--workspace,-p $(crate)) --all-features # Run Rust tests of Book. @@ -92,7 +91,7 @@ test.cargo: # make test.book test.book: - cargo +stable test --manifest-path book/tests/Cargo.toml + cargo test --manifest-path book/tests/Cargo.toml diff --git a/book/tests/Cargo.toml b/book/tests/Cargo.toml index dadbdb86..b9a1f3b4 100644 --- a/book/tests/Cargo.toml +++ b/book/tests/Cargo.toml @@ -15,7 +15,7 @@ async-trait = "0.1" cucumber = { version = "0.10", path = "../.." } futures = "0.3" skeptic = "0.13" -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } [build-dependencies] skeptic = "0.13" diff --git a/src/cli.rs b/src/cli.rs index ffb2e9a2..1185b691 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,8 +16,9 @@ use structopt::StructOpt; /// Run the tests, pet a dog!. #[derive(Debug, StructOpt)] -pub struct Opts +pub struct Opts where + Custom: StructOpt, Parser: StructOpt, Runner: StructOpt, Writer: StructOpt, @@ -41,6 +42,10 @@ where )] pub tags_filter: Option, + /// Custom CLI options. + #[structopt(flatten)] + pub custom: Custom, + /// [`Parser`] CLI options. /// /// [`Parser`]: crate::Parser diff --git a/src/cucumber.rs b/src/cucumber.rs index 0d25d94d..d257685b 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -22,7 +22,7 @@ use std::{ use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; -use structopt::StructOpt as _; +use structopt::StructOpt; use crate::{ cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, @@ -696,7 +696,7 @@ where + 'static, { self.filter_run_with_cli( - cli::Opts::::from_args(), + cli::Opts::::from_args(), input, filter, ) @@ -706,7 +706,8 @@ where /// Runs [`Cucumber`] with [`Scenario`]s filter with provided CLI options. /// /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` options with [`cli::Opts`] using [`cli::Compose`]. + /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic + /// parameter in [`cli::Opts`]. /// /// # Example /// @@ -730,9 +731,9 @@ where /// # } /// # /// # let fut = async { - /// let (_custom, cli) = - /// cli::Compose::>::from_args() - /// .into_inner(); + /// let cli = cli::Opts::::from_args(); + /// // ^ but something more meaningful :) + /// // Work with cli.custom /// /// MyWorld::cucumber() /// .filter_run_with_cli(cli, "tests/features/readme", |_, _, sc| { @@ -745,13 +746,14 @@ where /// ``` /// /// [`Scenario`]: gherkin::Scenario - pub async fn filter_run_with_cli( + pub async fn filter_run_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, filter: F, ) -> Wr where + Cli: StructOpt, F: Fn( &gherkin::Feature, Option<&gherkin::Rule>, @@ -765,6 +767,7 @@ where parser: parser_cli, runner: runner_cli, writer: writer_cli, + .. } = cli; let filter = move |f: &gherkin::Feature, @@ -1096,7 +1099,8 @@ where /// produces events handled by a [`Writer`]. /// /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` options with [`cli::Opts`] using [`cli::Compose`]. + /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic + /// parameter in [`cli::Opts`]. /// /// # Panics /// @@ -1125,9 +1129,9 @@ where /// # } /// # /// # let fut = async { - /// let (_custom, cli) = - /// cli::Compose::>::from_args() - /// .into_inner(); + /// let cli = cli::Opts::::from_args(); + /// // ^ but something more meaningful :) + /// // Work with cli.custom /// /// MyWorld::cucumber() /// .run_and_exit_with_cli(cli, "tests/features/readme") @@ -1140,11 +1144,13 @@ where /// [`Failed`]: crate::event::Step::Failed /// [`Feature`]: gherkin::Feature /// [`Step`]: gherkin::Step - pub async fn run_and_exit_with_cli( + pub async fn run_and_exit_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, - ) { + ) where + Cli: StructOpt, + { self.filter_run_and_exit_with_cli(cli, input, |_, _, _| true) .await; } @@ -1232,7 +1238,8 @@ where /// Runs [`Cucumber`] with [`Scenario`]s filter and provided [`cli::Opts`]. /// /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` options with [`cli::Opts`] using [`cli::Compose`]. + /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic + /// parameter in [`cli::Opts`]. /// /// # Panics /// @@ -1262,9 +1269,9 @@ where /// # } /// # /// # let fut = async { - /// let (_custom, cli) = - /// cli::Compose::>::from_args() - /// .into_inner(); + /// let cli = cli::Opts::::from_args(); + /// // ^ but something more meaningful :) + /// // Work with cli.custom /// /// MyWorld::cucumber() /// .filter_run_and_exit_with_cli( @@ -1302,12 +1309,13 @@ where /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario /// [`Step`]: crate::Step - pub async fn filter_run_and_exit_with_cli( + pub async fn filter_run_and_exit_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, filter: Filter, ) where + Cli: StructOpt, Filter: Fn( &gherkin::Feature, Option<&gherkin::Rule>, diff --git a/tests/wait.rs b/tests/wait.rs index 95756ac2..d4394e20 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -8,9 +8,7 @@ use tokio::time; #[tokio::main] async fn main() { - let (_custom, cli) = - cli::Compose::>::from_args() - .into_inner(); + let cli = cli::Opts::::from_args(); let res = World::cucumber() .before(|_, _, _, w| { From 2e83a94b4af95ec467913123be5c03ce112f6b8d Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 14:02:59 +0300 Subject: [PATCH 14/27] Mention CLI in Features chapter of the book --- book/src/Features.md | 52 ++++++++++++++++++++++++++++++++++++++++++++ src/cucumber.rs | 21 ++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/book/src/Features.md b/book/src/Features.md index 84092ce2..cfe47ee6 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -382,5 +382,57 @@ World::cucumber() +## CLI options + +`Cucumber` provides several options that can be passed to on the command-line. + +Pass the `--help` option to print out all the available configuration options: + +``` +cargo test --test -- --help +``` + +Default output is: + +``` +cucumber 0.10.0 +Run the tests, pet a dog! + +USAGE: + cucumber [FLAGS] [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + --verbose Outputs Step's Doc String, if present + +OPTIONS: + -c, --colors Indicates, whether output should be colored or not [default: auto] + -f, --features Feature-files glob pattern + --concurrent Number of concurrent scenarios + -n, --name Regex to select scenarios from [aliases: scenario-name] + -t, --tags Regex to select scenarios from [aliases: scenario-tags] +``` + +Example with [tag expressions](https://cucumber.io/docs/cucumber/api/#tag-expressions) for filtering Scenarios: + +``` +cargo test --test -- --tags='@cat or @dog or @ferris' +``` + +> Note: CLI overrides options set in the code. + + +### Customizing CLI options + +All `CLI` options are designed to be composable. + +For example all building-block traits have `CLI` associated type: [`Parser::CLI`](https://docs.rs/cucumber/*/cucumber/trait.Parser.html#associatedtype.CLI), [`Runner::CLI`](https://docs.rs/cucumber/*/cucumber/trait.Runner.html#associatedtype.CLI) and [`Writer::CLI`](https://docs.rs/cucumber/*/cucumber/trait.Writer.html#associatedtype.CLI). All of them are composed into a single `CLI`. + +In case you want to add completely custom `CLI` options, check out [`Cucumber::run_and_exit_with_cli()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.run_and_exit_with_cli) method. + + + + [Cucumber]: https://cucumber.io [Gherkin]: https://cucumber.io/docs/gherkin diff --git a/src/cucumber.rs b/src/cucumber.rs index d257685b..6f31a9e6 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -627,6 +627,27 @@ where self.filter_run(input, |_, _, _| true).await } + /// Runs [`Cucumber`] with provided CLI options. + /// + /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which + /// produces events handled by a [`Writer`]. + /// + /// This method exists not to hijack console and give users an ability to + /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic + /// parameter in [`cli::Opts`]. + /// + /// [`Feature`]: gherkin::Feature + pub async fn run_with_cli( + self, + cli: cli::Opts, + input: I, + ) -> Wr + where + Cli: StructOpt, + { + self.filter_run_with_cli(cli, input, |_, _, _| true).await + } + /// Runs [`Cucumber`] with [`Scenario`]s filter. /// /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which From d10414750c5cc1542ad336bbb7157cc76757b8c3 Mon Sep 17 00:00:00 2001 From: ilslv Date: Thu, 21 Oct 2021 14:22:38 +0300 Subject: [PATCH 15/27] Bump version to 0.11 and add CHANGELOG entry --- CHANGELOG.md | 14 ++++++++++++++ Cargo.toml | 4 ++-- book/src/Features.md | 2 +- book/src/Getting_Started.md | 4 ++-- book/tests/Cargo.toml | 2 +- codegen/Cargo.toml | 2 +- src/writer/basic.rs | 4 ++-- 7 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6ecb96..3aa4e548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ All user visible changes to `cucumber` crate will be documented in this file. Th +## [0.11.0] · 2021-??-?? +[0.11.0]: /../../tree/v0.11.0 + +[Diff](/../../compare/v0.10.0...v0.11.0) + +### BC Breaks + +- Complete `CLI` redesign ([#144]) + +[#144]: /../../pull/144 + + + + ## [0.10.0] · 2021-??-?? [0.10.0]: /../../tree/v0.10.0 diff --git a/Cargo.toml b/Cargo.toml index 59c6075c..0c52bb75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cucumber" -version = "0.10.0" +version = "0.11.0" edition = "2018" resolver = "2" description = """\ @@ -46,7 +46,7 @@ sealed = "0.3" structopt = "0.3.25" # "macros" feature dependencies -cucumber-codegen = { version = "0.10", path = "./codegen", optional = true } +cucumber-codegen = { version = "0.11", path = "./codegen", optional = true } inventory = { version = "0.1.10", optional = true } [dev-dependencies] diff --git a/book/src/Features.md b/book/src/Features.md index cfe47ee6..2aabea2e 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -395,7 +395,7 @@ cargo test --test -- --help Default output is: ``` -cucumber 0.10.0 +cucumber 0.11.0 Run the tests, pet a dog! USAGE: diff --git a/book/src/Getting_Started.md b/book/src/Getting_Started.md index 16321fe9..1b0fc5bb 100644 --- a/book/src/Getting_Started.md +++ b/book/src/Getting_Started.md @@ -9,7 +9,7 @@ Add this to your `Cargo.toml`: ```toml [dev-dependencies] async-trait = "0.1" -cucumber = "0.10" +cucumber = "0.11" futures = "0.3" [[test]] @@ -434,7 +434,7 @@ For that switch `futures` for `tokio` in dependencies: ```toml [dev-dependencies] async-trait = "0.1" -cucumber = "0.10" +cucumber = "0.11" tokio = { version = "1.10", features = ["macros", "rt-multi-thread", "time"] } [[test]] diff --git a/book/tests/Cargo.toml b/book/tests/Cargo.toml index b9a1f3b4..bbbb54f3 100644 --- a/book/tests/Cargo.toml +++ b/book/tests/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] async-trait = "0.1" -cucumber = { version = "0.10", path = "../.." } +cucumber = { version = "0.11", path = "../.." } futures = "0.3" skeptic = "0.13" tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index a27c851d..a68252fe 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cucumber-codegen" -version = "0.10.0" # should be the same as main crate version +version = "0.11.0" # should be the same as main crate version edition = "2018" resolver = "2" description = "Code generation for `cucumber` crate." diff --git a/src/writer/basic.rs b/src/writer/basic.rs index f7f72b1f..e2a8ead4 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -838,8 +838,8 @@ pub(crate) fn coerce_error(err: &Info) -> Cow<'static, str> { } } -/// Formats the given [`str`] by adding `indent`s to each line to prettify -/// the output. +/// Formats the given [`str`] by adding `indent`s to each line to prettify the +/// output. fn format_str_with_indent(str: impl AsRef, indent: usize) -> String { let str = str .as_ref() From 75f542a76d10138a5f453e655c717a6fdcaa9cc2 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 21 Oct 2021 20:16:45 +0300 Subject: [PATCH 16/27] Some corrections [skip ci] --- Cargo.toml | 4 +-- Makefile | 2 +- book/tests/Cargo.toml | 2 +- codegen/Cargo.toml | 2 +- src/cucumber.rs | 14 ++++----- src/parser/basic.rs | 72 +++++++++++++++++++++++-------------------- src/parser/mod.rs | 18 ++++++----- src/tag.rs | 8 ++--- 8 files changed, 64 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c52bb75..59c6075c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cucumber" -version = "0.11.0" +version = "0.10.0" edition = "2018" resolver = "2" description = """\ @@ -46,7 +46,7 @@ sealed = "0.3" structopt = "0.3.25" # "macros" feature dependencies -cucumber-codegen = { version = "0.11", path = "./codegen", optional = true } +cucumber-codegen = { version = "0.10", path = "./codegen", optional = true } inventory = { version = "0.1.10", optional = true } [dev-dependencies] diff --git a/Makefile b/Makefile index 44c3d98a..dae5a41d 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ cargo.fmt: # make cargo.lint cargo.lint: - cargo +stable clippy --workspace -- -D warnings + cargo clippy --workspace -- -D warnings diff --git a/book/tests/Cargo.toml b/book/tests/Cargo.toml index bbbb54f3..b9a1f3b4 100644 --- a/book/tests/Cargo.toml +++ b/book/tests/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] async-trait = "0.1" -cucumber = { version = "0.11", path = "../.." } +cucumber = { version = "0.10", path = "../.." } futures = "0.3" skeptic = "0.13" tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index a68252fe..a27c851d 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cucumber-codegen" -version = "0.11.0" # should be the same as main crate version +version = "0.10.0" # should be the same as main crate version edition = "2018" resolver = "2" description = "Code generation for `cucumber` crate." diff --git a/src/cucumber.rs b/src/cucumber.rs index 6f31a9e6..effe84f8 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -25,8 +25,8 @@ use regex::Regex; use structopt::StructOpt; use crate::{ - cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, - FailureWriter, Parser, Runner, ScenarioType, Step, World, Writer, + ArbitraryWriter, cli, event, FailureWriter, parser, Parser, runner, Runner, + ScenarioType, step, Step, tag::Ext as _, World, writer, Writer, WriterExt as _, }; @@ -639,7 +639,7 @@ where /// [`Feature`]: gherkin::Feature pub async fn run_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, ) -> Wr where @@ -717,7 +717,7 @@ where + 'static, { self.filter_run_with_cli( - cli::Opts::::from_args(), + cli::Opts::::from_args(), input, filter, ) @@ -769,7 +769,7 @@ where /// [`Scenario`]: gherkin::Scenario pub async fn filter_run_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, filter: F, ) -> Wr @@ -1167,7 +1167,7 @@ where /// [`Step`]: gherkin::Step pub async fn run_and_exit_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, ) where Cli: StructOpt, @@ -1332,7 +1332,7 @@ where /// [`Step`]: crate::Step pub async fn filter_run_and_exit_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, filter: Filter, ) where diff --git a/src/parser/basic.rs b/src/parser/basic.rs index b53e85bb..1a0febf5 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -12,6 +12,7 @@ use std::{ borrow::Cow, + fmt, path::{Path, PathBuf}, str::FromStr, vec, @@ -27,6 +28,20 @@ use crate::feature::Ext as _; use super::{Error as ParseError, Parser}; +// Workaround for overwritten doc comments: +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(doc, doc = "CLI options of [`Basic`] [`Parser`].")] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) +)] +#[derive(Debug, StructOpt)] +pub struct Cli { + /// `.feature` files glob pattern. + #[structopt(long, short, name = "glob")] + pub features: Option, +} + /// Default [`Parser`]. /// /// As there is no async runtime-agnostic way to interact with IO, this @@ -39,57 +54,29 @@ pub struct Basic { language: Option>, } -// Workaround for overwritten doc-comments. -// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 -#[cfg_attr( - not(doc), - allow(missing_docs, clippy::missing_docs_in_private_items) -)] -#[cfg_attr(doc, doc = "CLI options of [`Basic`].")] -#[allow(missing_debug_implementations)] -#[derive(StructOpt)] -pub struct CLI { - /// Feature-files glob pattern. - #[structopt(long, short, name = "glob")] - pub features: Option, -} - -/// [`GlobWalker`] wrapper with [`FromStr`] impl. -#[allow(missing_debug_implementations)] -pub struct Walker(GlobWalker); - -impl FromStr for Walker { - type Err = globwalk::GlobError; - - fn from_str(s: &str) -> Result { - globwalk::glob(s).map(Walker) - } -} - impl> Parser for Basic { - type CLI = CLI; + type Cli = Cli; type Output = stream::Iter>>; - fn parse(self, path: I, cli: Self::CLI) -> Self::Output { + fn parse(self, path: I, cli: Self::Cli) -> Self::Output { let walk = |walker: GlobWalker| { walker .filter_map(Result::ok) - .filter(|entry| { - entry - .path() + .filter(|file| { + file.path() .extension() .map(|ext| ext == "feature") .unwrap_or_default() }) - .map(|entry| { + .map(|file| { let env = self .language .as_ref() .and_then(|l| GherkinEnv::new(l).ok()) .unwrap_or_default(); - gherkin::Feature::parse_path(entry.path(), env) + gherkin::Feature::parse_path(file.path(), env) }) .collect::>() }; @@ -184,3 +171,20 @@ impl Basic { pub struct UnsupportedLanguageError( #[error(not(source))] pub Cow<'static, str>, ); + +/// Wrapper over [`GlobWalker`] with a [`FromStr`] impl. +pub struct Walker(GlobWalker); + +impl fmt::Debug for Walker { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Walker").finish_non_exhaustive() + } +} + +impl FromStr for Walker { + type Err = globwalk::GlobError; + + fn from_str(s: &str) -> Result { + globwalk::glob(s).map(Self) + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e5d9b0a4..6bc2e1e8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12,8 +12,6 @@ //! //! [Gherkin]: https://cucumber.io/docs/gherkin/reference -pub mod basic; - use std::sync::Arc; use derive_more::{Display, Error}; @@ -25,21 +23,25 @@ use crate::feature::ExpandExamplesError; #[doc(inline)] pub use self::basic::Basic; +pub mod basic; + /// Source of parsed [`Feature`]s. /// /// [`Feature`]: gherkin::Feature pub trait Parser { - /// [`StructOpt`] deriver for CLI options of this [`Parser`]. In case no - /// options present, use [`cli::Empty`]. + /// CLI options of this [`Parser`]. In case no options should be introduced, + /// just use [`cli::Empty`]. /// /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be - /// merged together, so overlapping arguments will cause runtime panic. + /// merged together, so overlapping arguments will cause a runtime panic. /// /// [`cli::Empty`]: crate::cli::Empty /// [`Runner`]: crate::Runner - /// [`StructOpt`]: structopt::StructOpt /// [`Writer`]: crate::Writer - type CLI: StructOptInternal + 'static; + // We do use `StructOptInternal` here only because `StructOpt::from_args()` + // requires exactly this trait bound. We don't touch any `StructOptInternal` + // details being a subject of instability. + type Cli: StructOptInternal + 'static; /// Output [`Stream`] of parsed [`Feature`]s. /// @@ -49,7 +51,7 @@ pub trait Parser { /// Parses the given `input` into a [`Stream`] of [`Feature`]s. /// /// [`Feature`]: gherkin::Feature - fn parse(self, input: I, cli: Self::CLI) -> Self::Output; + fn parse(self, input: I, cli: Self::Cli) -> Self::Output; } /// Result of parsing [Gherkin] files. diff --git a/src/tag.rs b/src/tag.rs index 26b8a307..4f52be64 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -24,10 +24,10 @@ pub trait Ext { impl Ext for TagOperation { fn eval(&self, tags: &[String]) -> bool { match self { - TagOperation::And(l, r) => l.eval(tags) & r.eval(tags), - TagOperation::Or(l, r) => l.eval(tags) | r.eval(tags), - TagOperation::Not(tag) => !tag.eval(tags), - TagOperation::Tag(t) => tags.iter().any(|tag| tag == t), + Self::And(l, r) => l.eval(tags) & r.eval(tags), + Self::Or(l, r) => l.eval(tags) | r.eval(tags), + Self::Not(tag) => !tag.eval(tags), + Self::Tag(t) => tags.iter().any(|tag| tag == t), } } } From cf8e669d793219e10f089cd49b77d482b082cc94 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 22 Oct 2021 10:01:30 +0300 Subject: [PATCH 17/27] Corrections --- CHANGELOG.md | 16 +---- README.md | 2 +- book/src/Features.md | 10 ++-- book/src/Getting_Started.md | 4 +- codegen/src/lib.rs | 3 + src/cli.rs | 110 +++++++++++++++++++++++++++++++--- src/cucumber.rs | 26 ++++---- src/runner/basic.rs | 8 +-- src/runner/mod.rs | 13 ++-- src/writer/basic.rs | 32 +++++----- src/writer/fail_on_skipped.rs | 4 +- src/writer/mod.rs | 13 ++-- src/writer/normalized.rs | 14 ++--- src/writer/repeat.rs | 4 +- src/writer/summarized.rs | 4 +- tests/output.rs | 4 +- tests/wait.rs | 7 +-- 17 files changed, 179 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa4e548..ce202912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,6 @@ All user visible changes to `cucumber` crate will be documented in this file. Th -## [0.11.0] · 2021-??-?? -[0.11.0]: /../../tree/v0.11.0 - -[Diff](/../../compare/v0.10.0...v0.11.0) - -### BC Breaks - -- Complete `CLI` redesign ([#144]) - -[#144]: /../../pull/144 - - - - ## [0.10.0] · 2021-??-?? [0.10.0]: /../../tree/v0.10.0 @@ -35,7 +21,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - Replaced `#[given(step)]`, `#[when(step)]` and `#[then(step)]` function argument attributes with a single `#[step]`. ([#128]) - Made test callbacks first argument `&mut World` instead of `World`. ([#128]) - Made `#[step]` argument of step functions `Step` instead of `StepContext` again, while test callbacks still receive `StepContext` as a second parameter. ([#128]) -- Deprecated `--nocapture` and `--debug` CLI options to be completely redesigned in `0.11` release. ([#137]) +- Complete `CLI` redesign ([#144]) - [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept optional `&mut World` as their last parameter. ([#142]) ### Added diff --git a/README.md b/README.md index a3ac56b6..31978178 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ struct World { capacity: usize, } -#[async_trait(? Send)] +#[async_trait(?Send)] impl cucumber::World for World { type Error = Infallible; diff --git a/book/src/Features.md b/book/src/Features.md index 2aabea2e..17ac42bd 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -395,7 +395,7 @@ cargo test --test -- --help Default output is: ``` -cucumber 0.11.0 +cucumber 0.10.0 Run the tests, pet a dog! USAGE: @@ -408,10 +408,10 @@ FLAGS: OPTIONS: -c, --colors Indicates, whether output should be colored or not [default: auto] - -f, --features Feature-files glob pattern + -f, --features `.feature` files glob pattern --concurrent Number of concurrent scenarios - -n, --name Regex to select scenarios from [aliases: scenario-name] - -t, --tags Regex to select scenarios from [aliases: scenario-tags] + -n, --name Regex to filter scenarios with [aliases: scenario-name] + -t, --tags Tag expression to filter scenarios with [aliases: scenario-tags] ``` Example with [tag expressions](https://cucumber.io/docs/cucumber/api/#tag-expressions) for filtering Scenarios: @@ -427,7 +427,7 @@ cargo test --test -- --tags='@cat or @dog or @ferris' All `CLI` options are designed to be composable. -For example all building-block traits have `CLI` associated type: [`Parser::CLI`](https://docs.rs/cucumber/*/cucumber/trait.Parser.html#associatedtype.CLI), [`Runner::CLI`](https://docs.rs/cucumber/*/cucumber/trait.Runner.html#associatedtype.CLI) and [`Writer::CLI`](https://docs.rs/cucumber/*/cucumber/trait.Writer.html#associatedtype.CLI). All of them are composed into a single `CLI`. +For example all building-block traits have `CLI` associated type: [`Parser::Cli`](https://docs.rs/cucumber/*/cucumber/trait.Parser.html#associatedtype.Cli), [`Runner::Cli`](https://docs.rs/cucumber/*/cucumber/trait.Runner.html#associatedtype.Cli) and [`Writer::Cli`](https://docs.rs/cucumber/*/cucumber/trait.Writer.html#associatedtype.Cli). All of them are composed into a single `CLI`. In case you want to add completely custom `CLI` options, check out [`Cucumber::run_and_exit_with_cli()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.run_and_exit_with_cli) method. diff --git a/book/src/Getting_Started.md b/book/src/Getting_Started.md index 1b0fc5bb..16321fe9 100644 --- a/book/src/Getting_Started.md +++ b/book/src/Getting_Started.md @@ -9,7 +9,7 @@ Add this to your `Cargo.toml`: ```toml [dev-dependencies] async-trait = "0.1" -cucumber = "0.11" +cucumber = "0.10" futures = "0.3" [[test]] @@ -434,7 +434,7 @@ For that switch `futures` for `tokio` in dependencies: ```toml [dev-dependencies] async-trait = "0.1" -cucumber = "0.11" +cucumber = "0.10" tokio = { version = "1.10", features = ["macros", "rt-multi-thread", "time"] } [[test]] diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 2f8f4dcc..a3f0b6f6 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -98,6 +98,7 @@ mod derive; use proc_macro::TokenStream; +/// Expands `given`, `when` and `then` proc-macro attributes. macro_rules! step_attribute { ($name:ident) => { /// Attribute to auto-wire the test to the [`World`] implementer. @@ -195,6 +196,8 @@ macro_rules! step_attribute { }; } +/// Expands `WorldInit` derive proc-macro and `given`, `when`, `then` proc-macro +/// attributes. macro_rules! steps { ($($name:ident),*) => { /// Derive macro for tests auto-wiring. diff --git a/src/cli.rs b/src/cli.rs index 1185b691..0f78c55f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,7 +8,16 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! CLI options. +//! Tools for composing CLI options. +//! +//! Main part of this module is [`Opts`], which composes all strongly-typed +//! `CLI` options from [`Parser`], [`Runner`] and [`Writer`] and adds filtering +//! based on [`Regex`] or [`Tag Expressions`][1]. +//! +//! [1]: https://cucumber.io/docs/cucumber/api/#tag-expressions +//! [`Parser`]: crate::Parser +//! [`Runner`]: crate::Runner +//! [`Writer`]: crate::Writer use gherkin::tagexpr::TagOperation; use regex::Regex; @@ -16,14 +25,14 @@ use structopt::StructOpt; /// Run the tests, pet a dog!. #[derive(Debug, StructOpt)] -pub struct Opts +pub struct Opts where Custom: StructOpt, Parser: StructOpt, Runner: StructOpt, Writer: StructOpt, { - /// Regex to select scenarios from. + /// Regex to filter scenarios with. #[structopt( short = "n", long = "name", @@ -32,7 +41,7 @@ where )] pub re_filter: Option, - /// Regex to select scenarios from. + /// Tag expression to filter scenarios with. #[structopt( short = "t", long = "tags", @@ -42,10 +51,6 @@ where )] pub tags_filter: Option, - /// Custom CLI options. - #[structopt(flatten)] - pub custom: Custom, - /// [`Parser`] CLI options. /// /// [`Parser`]: crate::Parser @@ -63,6 +68,10 @@ where /// [`Writer`]: crate::Writer #[structopt(flatten)] pub writer: Writer, + + /// Custom CLI options. + #[structopt(flatten)] + pub custom: Custom, } // Workaround for overwritten doc-comments. @@ -86,7 +95,90 @@ pub struct Empty { not(doc), allow(missing_docs, clippy::missing_docs_in_private_items) )] -#[cfg_attr(doc, doc = "Composes two [`StructOpt`] derivers together.")] +#[cfg_attr( + doc, + doc = r#" +Composes two [`StructOpt`] derivers together. + +# Example + +This struct is especially useful, when implementing custom [`Writer`], which +wraps another [`Writer`]. + +```rust +# use async_trait::async_trait; +# use cucumber::{ +# cli, event, parser, ArbitraryWriter, FailureWriter, World, Writer, +# }; +# use structopt::StructOpt; +# +struct CustomWriter(Wr); + +#[derive(StructOpt)] +struct Cli { + #[structopt(long)] + custom_option: Option, +} + +#[async_trait(?Send)] +impl Writer for CustomWriter +where + W: World, + Wr: Writer, +{ + type Cli = cli::Compose; + + async fn handle_event( + &mut self, + ev: parser::Result>, + cli: &Self::Cli, + ) { + // Some custom logic including `cli.left.custom_option`. + + self.0.handle_event(ev, &cli.right).await; + } +} + +// useful blanket impls + +#[async_trait(?Send)] +impl<'val, W, Wr, Val> ArbitraryWriter<'val, W, Val> for CustomWriter +where + W: World, + Self: Writer, + Wr: ArbitraryWriter<'val, W, Val>, + Val: 'val, +{ + async fn write(&mut self, val: Val) + where + 'val: 'async_trait, + { + self.0.write(val).await; + } +} + +impl FailureWriter for CustomWriter +where + W: World, + Self: Writer, + Wr: FailureWriter, +{ + fn failed_steps(&self) -> usize { + self.0.failed_steps() + } + + fn parsing_errors(&self) -> usize { + self.0.parsing_errors() + } + + fn hook_errors(&self) -> usize { + self.0.hook_errors() + } +} +``` + +[`Writer`]: crate::Writer"# +)] #[derive(Debug, StructOpt)] pub struct Compose where diff --git a/src/cucumber.rs b/src/cucumber.rs index effe84f8..a2c93c2e 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -25,8 +25,8 @@ use regex::Regex; use structopt::StructOpt; use crate::{ - ArbitraryWriter, cli, event, FailureWriter, parser, Parser, runner, Runner, - ScenarioType, step, Step, tag::Ext as _, World, writer, Writer, + cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, + FailureWriter, Parser, Runner, ScenarioType, Step, World, Writer, WriterExt as _, }; @@ -639,7 +639,7 @@ where /// [`Feature`]: gherkin::Feature pub async fn run_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, ) -> Wr where @@ -717,7 +717,7 @@ where + 'static, { self.filter_run_with_cli( - cli::Opts::::from_args(), + cli::Opts::::from_args(), input, filter, ) @@ -752,8 +752,8 @@ where /// # } /// # /// # let fut = async { - /// let cli = cli::Opts::::from_args(); - /// // ^ but something more meaningful :) + /// let cli = cli::Opts::<_, _, _, cli::Empty>::from_args(); + /// // but something more meaningful :) ^ /// // Work with cli.custom /// /// MyWorld::cucumber() @@ -769,7 +769,7 @@ where /// [`Scenario`]: gherkin::Scenario pub async fn filter_run_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, filter: F, ) -> Wr @@ -1150,8 +1150,8 @@ where /// # } /// # /// # let fut = async { - /// let cli = cli::Opts::::from_args(); - /// // ^ but something more meaningful :) + /// let cli = cli::Opts::<_, _, _, cli::Empty>::from_args(); + /// // but something more meaningful :) ^ /// // Work with cli.custom /// /// MyWorld::cucumber() @@ -1167,7 +1167,7 @@ where /// [`Step`]: gherkin::Step pub async fn run_and_exit_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, ) where Cli: StructOpt, @@ -1290,8 +1290,8 @@ where /// # } /// # /// # let fut = async { - /// let cli = cli::Opts::::from_args(); - /// // ^ but something more meaningful :) + /// let cli = cli::Opts::<_, _, _, cli::Empty>::from_args(); + /// // but something more meaningful :) ^ /// // Work with cli.custom /// /// MyWorld::cucumber() @@ -1332,7 +1332,7 @@ where /// [`Step`]: crate::Step pub async fn filter_run_and_exit_with_cli( self, - cli: cli::Opts, + cli: cli::Opts, input: I, filter: Filter, ) where diff --git a/src/runner/basic.rs b/src/runner/basic.rs index e39c376b..6f67f4cb 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -148,9 +148,9 @@ pub struct Basic< not(doc), allow(missing_docs, clippy::missing_docs_in_private_items) )] -#[cfg_attr(doc, doc = "CLI options of [`Basic`].")] +#[cfg_attr(doc, doc = "CLI options of [`Basic`] [`Runner`].")] #[derive(Clone, Copy, Debug, StructOpt)] -pub struct CLI { +pub struct Cli { /// Number of concurrent scenarios. #[structopt(long, name = "int")] pub concurrent: Option, @@ -383,12 +383,12 @@ where ) -> LocalBoxFuture<'a, ()> + 'static, { - type CLI = CLI; + type Cli = Cli; type EventStream = LocalBoxStream<'static, parser::Result>>; - fn run(self, features: S, cli: CLI) -> Self::EventStream + fn run(self, features: S, cli: Cli) -> Self::EventStream where S: Stream> + 'static, { diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 6af4f4d5..c444e17e 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -60,17 +60,20 @@ pub use self::basic::{Basic, ScenarioType}; /// /// [happened-before]: https://en.wikipedia.org/wiki/Happened-before pub trait Runner { - /// [`StructOpt`] deriver for CLI options of this [`Runner`]. In case no - /// options present, use [`cli::Empty`]. + /// CLI options of this [`Runner`]. In case no options should be introduced, + /// just use [`cli::Empty`]. /// /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be - /// merged together, so overlapping arguments will cause runtime panic. + /// merged together, so overlapping arguments will cause a runtime panic. /// /// [`cli::Empty`]: crate::cli::Empty /// [`Parser`]: crate::Parser /// [`StructOpt`]: structopt::StructOpt /// [`Writer`]: crate::Writer - type CLI: StructOptInternal; + // We do use `StructOptInternal` here only because `StructOpt::from_args()` + // requires exactly this trait bound. We don't touch any `StructOptInternal` + // details being a subject of instability. + type Cli: StructOptInternal; /// Output events [`Stream`]. type EventStream: Stream>>; @@ -80,7 +83,7 @@ pub trait Runner { /// /// [`Cucumber`]: event::Cucumber /// [`Feature`]: gherkin::Feature - fn run(self, features: S, cli: Self::CLI) -> Self::EventStream + fn run(self, features: S, cli: Self::Cli) -> Self::EventStream where S: Stream> + 'static; } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index e2a8ead4..1d41c3c7 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -58,9 +58,9 @@ pub struct Basic { not(doc), allow(missing_docs, clippy::missing_docs_in_private_items) )] -#[cfg_attr(doc, doc = "CLI options of [`Basic`].")] +#[cfg_attr(doc, doc = "CLI options of [`Basic`] [`Writer`].")] #[derive(Clone, Copy, Debug, StructOpt)] -pub struct CLI { +pub struct Cli { /// Outputs Step's Doc String, if present. #[structopt(long)] pub verbose: bool, @@ -104,13 +104,13 @@ impl FromStr for Colors { #[async_trait(?Send)] impl Writer for Basic { - type CLI = CLI; + type Cli = Cli; #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn handle_event( &mut self, ev: parser::Result>, - cli: &Self::CLI, + cli: &Self::Cli, ) { use event::{Cucumber, Feature}; @@ -219,7 +219,7 @@ impl Basic { feat: &gherkin::Feature, rule: &gherkin::Rule, ev: event::Rule, - cli: CLI, + cli: Cli, ) -> io::Result<()> { use event::Rule; @@ -263,7 +263,7 @@ impl Basic { feat: &gherkin::Feature, scenario: &gherkin::Scenario, ev: &event::Scenario, - cli: CLI, + cli: Cli, ) -> io::Result<()> { use event::{Hook, Scenario}; @@ -357,7 +357,7 @@ impl Basic { feat: &gherkin::Feature, step: &gherkin::Step, ev: &event::Step, - cli: CLI, + cli: Cli, ) -> io::Result<()> { use event::Step; @@ -394,7 +394,7 @@ impl Basic { fn step_started( &mut self, step: &gherkin::Step, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.indent += 4; if self.styles.is_present { @@ -431,7 +431,7 @@ impl Basic { &mut self, step: &gherkin::Step, captures: &CaptureLocations, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.clear_last_lines_if_term_present()?; @@ -479,7 +479,7 @@ impl Basic { &mut self, feat: &gherkin::Feature, step: &gherkin::Step, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.clear_last_lines_if_term_present()?; self.write_line(&self.styles.skipped(format!( @@ -519,7 +519,7 @@ impl Basic { captures: Option<&CaptureLocations>, world: Option<&W>, err: &event::StepError, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.clear_last_lines_if_term_present()?; @@ -595,7 +595,7 @@ impl Basic { feat: &gherkin::Feature, bg: &gherkin::Step, ev: &event::Step, - cli: CLI, + cli: Cli, ) -> io::Result<()> { use event::Step; @@ -633,7 +633,7 @@ impl Basic { fn bg_step_started( &mut self, step: &gherkin::Step, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.indent += 4; if self.styles.is_present { @@ -671,7 +671,7 @@ impl Basic { &mut self, step: &gherkin::Step, captures: &CaptureLocations, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.clear_last_lines_if_term_present()?; @@ -720,7 +720,7 @@ impl Basic { &mut self, feat: &gherkin::Feature, step: &gherkin::Step, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.clear_last_lines_if_term_present()?; self.write_line(&self.styles.skipped(format!( @@ -761,7 +761,7 @@ impl Basic { captures: Option<&CaptureLocations>, world: Option<&W>, err: &event::StepError, - cli: CLI, + cli: Cli, ) -> io::Result<()> { self.clear_last_lines_if_term_present()?; diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index 59a7f509..d51bb420 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -59,12 +59,12 @@ where ) -> bool, Wr: for<'val> ArbitraryWriter<'val, W, String>, { - type CLI = Wr::CLI; + type Cli = Wr::Cli; async fn handle_event( &mut self, ev: parser::Result>, - cli: &Self::CLI, + cli: &Self::Cli, ) { use event::{ Cucumber, Feature, Rule, Scenario, Step, StepError::Panic, diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 37d36648..c2874ede 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -42,17 +42,20 @@ pub use self::{ /// [`Cucumber::run_and_exit()`]: crate::Cucumber::run_and_exit #[async_trait(?Send)] pub trait Writer { - /// [`StructOpt`] deriver for CLI options of this [`Writer`]. In case no - /// options present, use [`cli::Empty`]. + /// CLI options of this [`Writer`]. In case no options should be introduced, + /// just use [`cli::Empty`]. /// /// All CLI options from [`Parser`], [`Runner`] and [`Writer`] will be - /// merged together, so overlapping arguments will cause runtime panic. + /// merged together, so overlapping arguments will cause a runtime panic. /// /// [`cli::Empty`]: crate::cli::Empty /// [`Parser`]: crate::Parser /// [`Runner`]: crate::Runner /// [`StructOpt`]: structopt::StructOpt - type CLI: StructOptInternal; + // We do use `StructOptInternal` here only because `StructOpt::from_args()` + // requires exactly this trait bound. We don't touch any `StructOptInternal` + // details being a subject of instability. + type Cli: StructOptInternal; /// Handles the given [`Cucumber`] event. /// @@ -60,7 +63,7 @@ pub trait Writer { async fn handle_event( &mut self, ev: parser::Result>, - cli: &Self::CLI, + cli: &Self::Cli, ); } diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 00e33137..d643f770 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -60,12 +60,12 @@ impl Normalized { #[async_trait(?Send)] impl> Writer for Normalized { - type CLI = Wr::CLI; + type Cli = Wr::Cli; async fn handle_event( &mut self, ev: parser::Result>, - cli: &Self::CLI, + cli: &Self::Cli, ) { use event::{Cucumber, Feature, Rule}; @@ -255,7 +255,7 @@ trait Emitter { self, path: Self::EmittedPath, writer: &mut W, - cli: &W::CLI, + cli: &W::Cli, ) -> Option; } @@ -342,7 +342,7 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { self, _: (), writer: &mut W, - cli: &W::CLI, + cli: &W::Cli, ) -> Option { if let Some((f, events)) = self.current_item() { if !events.is_started_emitted() { @@ -484,7 +484,7 @@ impl<'me, World> Emitter for &'me mut FeatureQueue { self, feature: Self::EmittedPath, writer: &mut W, - cli: &W::CLI, + cli: &W::Cli, ) -> Option { match self.current_item()? { Either::Left((rule, events)) => events @@ -521,7 +521,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { self, (feature, rule): Self::EmittedPath, writer: &mut W, - cli: &W::CLI, + cli: &W::Cli, ) -> Option { if !self.is_started_emitted() { writer @@ -599,7 +599,7 @@ impl Emitter for &mut ScenariosQueue { self, (feature, rule, scenario): Self::EmittedPath, writer: &mut W, - cli: &W::CLI, + cli: &W::Cli, ) -> Option { while let Some(ev) = self.current_item() { let should_be_removed = matches!(ev, event::Scenario::Finished); diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index dd802dc7..947ac3d5 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -49,12 +49,12 @@ where Wr: Writer, F: Fn(&parser::Result>) -> bool, { - type CLI = Wr::CLI; + type Cli = Wr::Cli; async fn handle_event( &mut self, ev: parser::Result>, - cli: &Self::CLI, + cli: &Self::Cli, ) { if (self.filter)(&ev) { self.events.push(ev.clone()); diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index eda01dd0..e7c543e3 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -141,12 +141,12 @@ where W: World, Wr: for<'val> ArbitraryWriter<'val, W, String>, { - type CLI = Wr::CLI; + type Cli = Wr::Cli; async fn handle_event( &mut self, ev: parser::Result>, - cli: &Self::CLI, + cli: &Self::Cli, ) { use event::{Cucumber, Feature, Rule}; diff --git a/tests/output.rs b/tests/output.rs index 7e7d1a06..bd54adce 100644 --- a/tests/output.rs +++ b/tests/output.rs @@ -36,12 +36,12 @@ struct DebugWriter(String); #[async_trait(?Send)] impl Writer for DebugWriter { - type CLI = cli::Empty; + type Cli = cli::Empty; async fn handle_event( &mut self, ev: parser::Result>, - _: &Self::CLI, + _: &Self::Cli, ) { use event::{Cucumber, Feature, Rule, Scenario, Step, StepError}; diff --git a/tests/wait.rs b/tests/wait.rs index d4394e20..ec87e380 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -1,15 +1,12 @@ use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; -use cucumber::{cli, given, then, when, WorldInit}; +use cucumber::{given, then, when, WorldInit}; use futures::FutureExt as _; -use structopt::StructOpt as _; use tokio::time; #[tokio::main] async fn main() { - let cli = cli::Opts::::from_args(); - let res = World::cucumber() .before(|_, _, _, w| { async move { @@ -21,7 +18,7 @@ async fn main() { .after(|_, _, _, _| { time::sleep(Duration::from_millis(10)).boxed_local() }) - .run_and_exit_with_cli(cli, "tests/features/wait"); + .run_and_exit("tests/features/wait"); let err = AssertUnwindSafe(res) .catch_unwind() From d345c4c2934f2131853424fefb337249f58f9611 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 22 Oct 2021 12:27:32 +0300 Subject: [PATCH 18/27] Move trait bound from impls to struct in Cucumber --- Cargo.toml | 1 + src/cucumber.rs | 826 ++++++++++++++++++++++-------------------------- tests/wait.rs | 26 +- 3 files changed, 402 insertions(+), 451 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 59c6075c..0740a997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ cucumber-codegen = { version = "0.10", path = "./codegen", optional = true } inventory = { version = "0.1.10", optional = true } [dev-dependencies] +humantime = "2.1" tokio = { version = "1.12", features = ["macros", "rt-multi-thread", "time"] } [[test]] diff --git a/src/cucumber.rs b/src/cucumber.rs index a2c93c2e..74449b7a 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -22,12 +22,11 @@ use std::{ use futures::{future::LocalBoxFuture, StreamExt as _}; use regex::Regex; -use structopt::StructOpt; +use structopt::{StructOpt, StructOptInternal}; use crate::{ - cli, event, parser, runner, step, tag::Ext as _, writer, ArbitraryWriter, - FailureWriter, Parser, Runner, ScenarioType, Step, World, Writer, - WriterExt as _, + cli, event, parser, runner, step, tag::Ext as _, writer, FailureWriter, + Parser, Runner, ScenarioType, Step, World, Writer, WriterExt as _, }; /// Top-level [Cucumber] executor. @@ -41,7 +40,7 @@ use crate::{ /// [`Cucumber::given()`], [`Cucumber::when()`] and [`Cucumber::then()`]. /// /// In case you want custom [`Parser`], [`Runner`] or [`Writer`] or -/// some other finer control, use [`Cucumber::custom()`] with +/// some other finer control, use [`Cucumber::custom()`] or /// [`Cucumber::with_parser()`], [`Cucumber::with_runner()`] and /// [`Cucumber::with_writer()`] to construct your dream [Cucumber] executor! /// @@ -49,7 +48,14 @@ use crate::{ /// [`WorldInit::collection()`]: crate::WorldInit::collection() /// [`WorldInit::cucumber()`]: crate::WorldInit::cucumber() /// [`WorldInit::run()`]: crate::WorldInit::run() -pub struct Cucumber { +pub struct Cucumber +where + W: World, + P: Parser, + R: Runner, + Wr: Writer, + CCli: StructOpt, +{ /// [`Parser`] sourcing [`Feature`]s for execution. /// /// [`Feature`]: gherkin::Feature @@ -63,6 +69,9 @@ pub struct Cucumber { /// [`Writer`] outputting [`event`]s to some output. writer: Wr, + /// `CLI` options. + cli: OptionalCli, + /// Type of the [`World`] this [`Cucumber`] run on. _world: PhantomData, @@ -70,30 +79,36 @@ pub struct Cucumber { _parser_input: PhantomData, } -impl Cucumber { - /// Creates an empty [`Cucumber`] executor. - /// - /// Use [`Cucumber::with_parser()`], [`Cucumber::with_runner()`] and - /// [`Cucumber::with_writer()`] to be able to [`Cucumber::run()`] it. +/// Type alias for [`Option`]`<`[`cli::Opts`]`>`. +type OptionalCli = Option>; + +impl Cucumber +where + W: World, + P: Parser, + R: Runner, + Wr: Writer, + CCli: StructOpt, +{ + /// Creates custom [`Cucumber`] executor. #[must_use] - pub const fn custom() -> Self { + pub fn custom(parser: P, runner: R, writer: Wr) -> Self { Self { - parser: (), - runner: (), - writer: (), + parser, + runner, + writer, + cli: None, _world: PhantomData, _parser_input: PhantomData, } } -} -impl Cucumber { /// Replaces [`Parser`]. #[must_use] pub fn with_parser( self, parser: NewP, - ) -> Cucumber + ) -> Cucumber where NewP: Parser, { @@ -102,6 +117,7 @@ impl Cucumber { parser, runner, writer, + cli: None, _world: PhantomData, _parser_input: PhantomData, } @@ -109,7 +125,10 @@ impl Cucumber { /// Replaces [`Runner`]. #[must_use] - pub fn with_runner(self, runner: NewR) -> Cucumber + pub fn with_runner( + self, + runner: NewR, + ) -> Cucumber where NewR: Runner, { @@ -118,6 +137,7 @@ impl Cucumber { parser, runner, writer, + cli: None, _world: PhantomData, _parser_input: PhantomData, } @@ -128,7 +148,7 @@ impl Cucumber { pub fn with_writer( self, writer: NewWr, - ) -> Cucumber + ) -> Cucumber where NewWr: Writer, { @@ -137,21 +157,13 @@ impl Cucumber { parser, runner, writer, + cli: None, _world: PhantomData, _parser_input: PhantomData, } } -} -impl Cucumber -where - W: World, - Wr: Writer, -{ - /// Consider [`Skipped`] steps as [`Failed`] if their [`Scenario`] isn't - /// marked with `@allow_skipped` tag. - /// - /// It's useful option for ensuring that all the steps were covered. + /// Re-outputs [`Skipped`] steps for easier navigation. /// /// # Example /// @@ -162,13 +174,13 @@ where /// async data-autoplay="true" data-rows="17"> /// /// - /// To fail all the [`Skipped`] steps setup [`Cucumber`] like this: - /// ```rust,should_panic + /// Adjust [`Cucumber`] to re-output all [`Skipped`] steps at the end: + /// ```rust /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use cucumber::WorldInit; /// # use futures::FutureExt as _; + /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] /// # struct MyWorld; @@ -184,7 +196,7 @@ where /// # /// # let fut = async { /// MyWorld::cucumber() - /// .fail_on_skipped() + /// .repeat_skipped() /// .run_and_exit("tests/features/readme") /// .await; /// # }; @@ -192,62 +204,37 @@ where /// # futures::executor::block_on(fut); /// ``` /// /// - /// To intentionally suppress some [`Skipped`] steps failing, use the - /// `@allow_skipped` tag: - /// ```gherkin - /// Feature: Animal feature - /// - /// Scenario: If we feed a hungry cat it will no longer be hungry - /// Given a hungry cat - /// When I feed the cat - /// Then the cat is not hungry - /// - /// @allow_skipped - /// Scenario: If we feed a satiated dog it will not become hungry - /// Given a satiated dog - /// When I feed the dog - /// Then the dog is not hungry - /// ``` - /// - /// [`Failed`]: crate::event::Step::Failed /// [`Scenario`]: gherkin::Scenario /// [`Skipped`]: crate::event::Step::Skipped #[must_use] - pub fn fail_on_skipped( + pub fn repeat_skipped( self, - ) -> Cucumber> { + ) -> Cucumber, CCli> { Cucumber { parser: self.parser, runner: self.runner, - writer: self.writer.fail_on_skipped(), + writer: self.writer.repeat_skipped(), + cli: self.cli, _world: PhantomData, _parser_input: PhantomData, } } - /// Consider [`Skipped`] steps as [`Failed`] if the given `filter` predicate - /// returns `true`. + /// Re-outputs [`Failed`] steps for easier navigation. /// /// # Example /// - /// Output with a regular [`Cucumber::run()`]: - /// - /// - /// Adjust [`Cucumber`] to fail on all [`Skipped`] steps, but the ones - /// marked with `@dog` tag: + /// Output with a regular [`Cucumber::fail_on_skipped()`]: /// ```rust,should_panic /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; + /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] @@ -264,85 +251,21 @@ where /// # /// # let fut = async { /// MyWorld::cucumber() - /// .fail_on_skipped_with(|_, _, s| !s.tags.iter().any(|t| t == "dog")) + /// .fail_on_skipped() /// .run_and_exit("tests/features/readme") /// .await; /// # }; /// # /// # futures::executor::block_on(fut); /// ``` - /// ```gherkin - /// Feature: Animal feature - /// - /// Scenario: If we feed a hungry cat it will no longer be hungry - /// Given a hungry cat - /// When I feed the cat - /// Then the cat is not hungry - /// - /// Scenario: If we feed a satiated dog it will not become hungry - /// Given a satiated dog - /// When I feed the dog - /// Then the dog is not hungry - /// ``` /// /// - /// And to avoid failing, use the `@dog` tag: - /// ```gherkin - /// Feature: Animal feature - /// - /// Scenario: If we feed a hungry cat it will no longer be hungry - /// Given a hungry cat - /// When I feed the cat - /// Then the cat is not hungry - /// - /// @dog - /// Scenario: If we feed a satiated dog it will not become hungry - /// Given a satiated dog - /// When I feed the dog - /// Then the dog is not hungry - /// ``` - /// - /// [`Failed`]: crate::event::Step::Failed - /// [`Scenario`]: gherkin::Scenario - /// [`Skipped`]: crate::event::Step::Skipped - #[must_use] - pub fn fail_on_skipped_with( - self, - filter: Filter, - ) -> Cucumber> - 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), - _world: PhantomData, - _parser_input: PhantomData, - } - } - - /// Re-outputs [`Skipped`] steps for easier navigation. - /// - /// # Example - /// - /// Output with a regular [`Cucumber::run()`]: - /// - /// - /// Adjust [`Cucumber`] to re-output all [`Skipped`] steps at the end: - /// ```rust + /// Adjust [`Cucumber`] to re-output all [`Failed`] steps at the end: + /// ```rust,should_panic /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; @@ -363,7 +286,8 @@ where /// # /// # let fut = async { /// MyWorld::cucumber() - /// .repeat_skipped() + /// .repeat_failed() + /// .fail_on_skipped() /// .run_and_exit("tests/features/readme") /// .await; /// # }; @@ -371,25 +295,36 @@ where /// # futures::executor::block_on(fut); /// ``` /// /// + /// > ⚠️ __WARNING__: [`Cucumber::repeat_failed()`] should be called before + /// [`Cucumber::fail_on_skipped()`], as events pass from + /// outer [`Writer`]s to inner ones. So we need to + /// transform [`Skipped`] to [`Failed`] first, and only + /// then [`Repeat`] them. + /// + /// [`Failed`]: crate::event::Step::Failed + /// [`Repeat`]: writer::Repeat /// [`Scenario`]: gherkin::Scenario /// [`Skipped`]: crate::event::Step::Skipped #[must_use] - pub fn repeat_skipped(self) -> Cucumber> { + pub fn repeat_failed( + self, + ) -> Cucumber, CCli> { Cucumber { parser: self.parser, runner: self.runner, - writer: self.writer.repeat_skipped(), + writer: self.writer.repeat_failed(), + cli: self.cli, _world: PhantomData, _parser_input: PhantomData, } } - /// Re-outputs [`Failed`] steps for easier navigation. + /// Re-output steps by the given `filter` predicate. /// /// # Example /// @@ -428,7 +363,8 @@ where /// async data-autoplay="true" data-rows="21"> /// /// - /// Adjust [`Cucumber`] to re-output all [`Failed`] steps at the end: + /// Adjust [`Cucumber`] to re-output all [`Failed`] steps ta the end by + /// providing a custom `filter` predicate: /// ```rust,should_panic /// # use std::convert::Infallible; /// # @@ -450,7 +386,28 @@ where /// # /// # let fut = async { /// MyWorld::cucumber() - /// .repeat_failed() + /// .repeat_if(|ev| { + /// use cucumber::event::{Cucumber, Feature, Rule, Scenario, Step}; + /// + /// matches!( + /// ev, + /// Ok(Cucumber::Feature( + /// _, + /// Feature::Rule( + /// _, + /// Rule::Scenario( + /// _, + /// Scenario::Step(_, Step::Failed(..)) + /// | Scenario::Background(_, Step::Failed(..)) + /// ) + /// ) | Feature::Scenario( + /// _, + /// Scenario::Step(_, Step::Failed(..)) + /// | Scenario::Background(_, Step::Failed(..)) + /// ) + /// )) | Err(_) + /// ) + /// }) /// .fail_on_skipped() /// .run_and_exit("tests/features/readme") /// .await; @@ -464,7 +421,7 @@ where /// async data-autoplay="true" data-rows="24"> /// /// - /// > ⚠️ __WARNING__: [`Cucumber::repeat_failed()`] should be called before + /// > ⚠️ __WARNING__: [`Cucumber::repeat_if()`] should be called before /// [`Cucumber::fail_on_skipped()`], as events pass from /// outer [`Writer`]s to inner ones. So we need to /// transform [`Skipped`] to [`Failed`] first, and only @@ -475,27 +432,53 @@ where /// [`Scenario`]: gherkin::Scenario /// [`Skipped`]: crate::event::Step::Skipped #[must_use] - pub fn repeat_failed(self) -> Cucumber> { + pub fn repeat_if( + self, + filter: F, + ) -> Cucumber, CCli> + where + F: Fn(&parser::Result>) -> bool, + { Cucumber { parser: self.parser, runner: self.runner, - writer: self.writer.repeat_failed(), + writer: self.writer.repeat_if(filter), + cli: self.cli, _world: PhantomData, _parser_input: PhantomData, } } +} - /// Re-output steps by the given `filter` predicate. +impl Cucumber +where + W: World, + P: Parser, + R: Runner, + Wr: Writer + for<'val> writer::Arbitrary<'val, W, String>, + CCli: StructOpt, +{ + /// Consider [`Skipped`] steps as [`Failed`] if their [`Scenario`] isn't + /// marked with `@allow_skipped` tag. + /// + /// It's useful option for ensuring that all the steps were covered. /// /// # Example /// - /// Output with a regular [`Cucumber::fail_on_skipped()`]: + /// Output with a regular [`Cucumber::run()`]: + /// + /// + /// To fail all the [`Skipped`] steps setup [`Cucumber`] like this: /// ```rust,should_panic /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; + /// # use futures::FutureExt as _; /// # /// # #[derive(Debug, WorldInit)] /// # struct MyWorld; @@ -519,18 +502,63 @@ where /// # futures::executor::block_on(fut); /// ``` /// /// - /// Adjust [`Cucumber`] to re-output all [`Failed`] steps ta the end by - /// providing a custom `filter` predicate: + /// To intentionally suppress some [`Skipped`] steps failing, use the + /// `@allow_skipped` tag: + /// ```gherkin + /// Feature: Animal feature + /// + /// Scenario: If we feed a hungry cat it will no longer be hungry + /// Given a hungry cat + /// When I feed the cat + /// Then the cat is not hungry + /// + /// @allow_skipped + /// Scenario: If we feed a satiated dog it will not become hungry + /// Given a satiated dog + /// When I feed the dog + /// Then the dog is not hungry + /// ``` + /// + /// [`Failed`]: crate::event::Step::Failed + /// [`Scenario`]: gherkin::Scenario + /// [`Skipped`]: crate::event::Step::Skipped + #[must_use] + pub fn fail_on_skipped( + self, + ) -> Cucumber, CCli> { + Cucumber { + parser: self.parser, + runner: self.runner, + writer: self.writer.fail_on_skipped(), + cli: self.cli, + _world: PhantomData, + _parser_input: PhantomData, + } + } + + /// Consider [`Skipped`] steps as [`Failed`] if the given `filter` predicate + /// returns `true`. + /// + /// # Example + /// + /// Output with a regular [`Cucumber::run()`]: + /// + /// + /// Adjust [`Cucumber`] to fail on all [`Skipped`] steps, but the ones + /// marked with `@dog` tag: /// ```rust,should_panic /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] @@ -547,75 +575,81 @@ where /// # /// # let fut = async { /// MyWorld::cucumber() - /// .repeat_if(|ev| { - /// use cucumber::event::{Cucumber, Feature, Rule, Scenario, Step}; - /// - /// matches!( - /// ev, - /// Ok(Cucumber::Feature( - /// _, - /// Feature::Rule( - /// _, - /// Rule::Scenario( - /// _, - /// Scenario::Step(_, Step::Failed(..)) - /// | Scenario::Background(_, Step::Failed(..)) - /// ) - /// ) | Feature::Scenario( - /// _, - /// Scenario::Step(_, Step::Failed(..)) - /// | Scenario::Background(_, Step::Failed(..)) - /// ) - /// )) | Err(_) - /// ) - /// }) - /// .fail_on_skipped() + /// .fail_on_skipped_with(|_, _, s| !s.tags.iter().any(|t| t == "dog")) /// .run_and_exit("tests/features/readme") /// .await; /// # }; /// # /// # futures::executor::block_on(fut); /// ``` + /// ```gherkin + /// Feature: Animal feature + /// + /// Scenario: If we feed a hungry cat it will no longer be hungry + /// Given a hungry cat + /// When I feed the cat + /// Then the cat is not hungry + /// + /// Scenario: If we feed a satiated dog it will not become hungry + /// Given a satiated dog + /// When I feed the dog + /// Then the dog is not hungry + /// ``` /// /// - /// > ⚠️ __WARNING__: [`Cucumber::repeat_if()`] should be called before - /// [`Cucumber::fail_on_skipped()`], as events pass from - /// outer [`Writer`]s to inner ones. So we need to - /// transform [`Skipped`] to [`Failed`] first, and only - /// then [`Repeat`] them. + /// And to avoid failing, use the `@dog` tag: + /// ```gherkin + /// Feature: Animal feature + /// + /// Scenario: If we feed a hungry cat it will no longer be hungry + /// Given a hungry cat + /// When I feed the cat + /// Then the cat is not hungry + /// + /// @dog + /// Scenario: If we feed a satiated dog it will not become hungry + /// Given a satiated dog + /// When I feed the dog + /// Then the dog is not hungry + /// ``` /// /// [`Failed`]: crate::event::Step::Failed - /// [`Repeat`]: writer::Repeat /// [`Scenario`]: gherkin::Scenario /// [`Skipped`]: crate::event::Step::Skipped #[must_use] - pub fn repeat_if( + pub fn fail_on_skipped_with( self, - filter: F, - ) -> Cucumber> + filter: Filter, + ) -> Cucumber, CCli> where - F: Fn(&parser::Result>) -> bool, + Filter: Fn( + &gherkin::Feature, + Option<&gherkin::Rule>, + &gherkin::Scenario, + ) -> bool, { Cucumber { parser: self.parser, runner: self.runner, - writer: self.writer.repeat_if(filter), + writer: self.writer.fail_on_skipped_with(filter), + cli: self.cli, _world: PhantomData, _parser_input: PhantomData, } } } -impl Cucumber +impl Cucumber where W: World, P: Parser, R: Runner, Wr: Writer, + CCli: StructOpt + StructOptInternal, { /// Runs [`Cucumber`]. /// @@ -627,40 +661,22 @@ where self.filter_run(input, |_, _, _| true).await } - /// Runs [`Cucumber`] with provided CLI options. - /// - /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which - /// produces events handled by a [`Writer`]. + /// Provides custom `CLI` options. /// /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic + /// compose custom `CLI` by providing [`StructOpt`] deriver as last generic /// parameter in [`cli::Opts`]. /// - /// [`Feature`]: gherkin::Feature - pub async fn run_with_cli( - self, - cli: cli::Opts, - input: I, - ) -> Wr - where - Cli: StructOpt, - { - self.filter_run_with_cli(cli, input, |_, _, _| true).await - } - - /// Runs [`Cucumber`] with [`Scenario`]s filter. - /// - /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which - /// produces events handled by a [`Writer`]. - /// /// # Example /// - /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag: /// ```rust - /// # use std::convert::Infallible; + /// # use std::{convert::Infallible, time::Duration}; /// # /// # use async_trait::async_trait; - /// # use cucumber::WorldInit; + /// # use cucumber::{cli, WorldInit}; + /// # use futures::FutureExt as _; + /// # use structopt::StructOpt; + /// # use tokio::time; /// # /// # #[derive(Debug, WorldInit)] /// # struct MyWorld; @@ -675,29 +691,42 @@ where /// # } /// # /// # let fut = async { + /// #[derive(StructOpt)] + /// struct Cli { + /// /// Time to wait in before hook. + /// #[structopt( + /// long, + /// parse(try_from_str = humantime::parse_duration) + /// )] + /// before_time: Option, + /// } + /// + /// let cli = cli::Opts::<_, _, _, Cli>::from_args(); + /// let time = cli + /// .custom + /// .before_time + /// .unwrap_or_default(); + /// /// MyWorld::cucumber() - /// .filter_run("tests/features/readme", |_, _, sc| { - /// sc.tags.iter().any(|t| t == "cat") - /// }) + /// .before(move |_, _, _, _| time::sleep(time).boxed_local()) + /// .with_cli(cli) + /// .run_and_exit("tests/features/readme") /// .await; /// # }; /// # - /// # futures::executor::block_on(fut); + /// # tokio::runtime::Builder::new_current_thread() + /// # .enable_all() + /// # .build() + /// # .unwrap() + /// # .block_on(fut); /// ``` /// ```gherkin /// Feature: Animal feature /// - /// @cat /// Scenario: If we feed a hungry cat it will no longer be hungry /// Given a hungry cat /// When I feed the cat /// Then the cat is not hungry - /// - /// @dog - /// Scenario: If we feed a satiated dog it will not become hungry - /// Given a satiated dog - /// When I feed the dog - /// Then the dog is not hungry /// ``` /// /// + /// [`Feature`]: gherkin::Feature /// [`Scenario`]: gherkin::Scenario - pub async fn filter_run_with_cli( - self, - cli: cli::Opts, - input: I, - filter: F, - ) -> Wr + pub async fn filter_run(self, input: I, filter: F) -> Wr where - Cli: StructOpt, F: Fn( &gherkin::Feature, Option<&gherkin::Rule>, @@ -789,7 +834,7 @@ where runner: runner_cli, writer: writer_cli, .. - } = cli; + } = self.cli.unwrap_or_else(cli::Opts::<_, _, _, _>::from_args); let filter = move |f: &gherkin::Feature, r: Option<&gherkin::Rule>, @@ -843,11 +888,13 @@ where } } -impl Debug for Cucumber +impl Debug for Cucumber where - P: Debug, - R: Debug, - Wr: Debug, + W: World, + P: Debug + Parser, + R: Debug + Runner, + Wr: Debug + Writer, + CCli: StructOpt, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Cucumber") @@ -873,10 +920,11 @@ where I: AsRef, { fn default() -> Self { - Cucumber::custom() - .with_parser(parser::Basic::new()) - .with_runner(runner::Basic::default()) - .with_writer(writer::Basic::new().normalized().summarized()) + Cucumber::custom( + parser::Basic::new(), + runner::Basic::default(), + writer::Basic::new().normalized().summarized(), + ) } } @@ -911,7 +959,14 @@ where } } -impl Cucumber { +impl Cucumber +where + W: World, + R: Runner, + Wr: Writer, + CCli: StructOpt, + I: AsRef, +{ /// Sets the provided language of [`gherkin`] files. /// /// # Errors @@ -926,7 +981,34 @@ impl Cucumber { } } -impl Cucumber, Wr> { +impl + Cucumber, Wr, CCli> +where + W: World, + P: Parser, + Wr: Writer, + CCli: StructOpt, + 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, +{ /// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will /// be limited. /// @@ -950,7 +1032,7 @@ impl Cucumber, Wr> { pub fn which_scenario( self, func: Which, - ) -> Cucumber, Wr> + ) -> Cucumber, Wr, CCli> where Which: Fn( &gherkin::Feature, @@ -963,12 +1045,14 @@ impl Cucumber, Wr> { parser, runner, writer, + cli, .. } = self; Cucumber { parser, runner: runner.which_scenario(func), writer, + cli, _world: PhantomData, _parser_input: PhantomData, } @@ -984,25 +1068,28 @@ impl Cucumber, Wr> { pub fn before( self, func: Before, - ) -> Cucumber, Wr> + ) -> Cucumber, Wr, CCli> where Before: for<'a> Fn( - &'a gherkin::Feature, - Option<&'a gherkin::Rule>, - &'a gherkin::Scenario, - &'a mut W, - ) -> LocalBoxFuture<'a, ()>, + &'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, } @@ -1028,25 +1115,28 @@ impl Cucumber, Wr> { pub fn after( self, func: After, - ) -> Cucumber, Wr> + ) -> Cucumber, Wr, CCli> where After: for<'a> Fn( - &'a gherkin::Feature, - Option<&'a gherkin::Rule>, - &'a gherkin::Scenario, - Option<&'a mut W>, - ) -> LocalBoxFuture<'a, ()>, + &'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, } @@ -1090,12 +1180,13 @@ impl Cucumber, Wr> { } } -impl Cucumber +impl Cucumber where W: World, P: Parser, R: Runner, - Wr: for<'val> ArbitraryWriter<'val, W, String> + FailureWriter, + Wr: FailureWriter, + CCli: StructOpt + StructOptInternal, { /// Runs [`Cucumber`]. /// @@ -1114,68 +1205,6 @@ where self.filter_run_and_exit(input, |_, _, _| true).await; } - /// Runs [`Cucumber`] with provided [`cli::Opts`]. - /// - /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which - /// produces events handled by a [`Writer`]. - /// - /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic - /// parameter in [`cli::Opts`]. - /// - /// # Panics - /// - /// If encountered errors while parsing [`Feature`]s or at least one - /// [`Step`] [`Failed`]. - /// - /// # Example - /// - /// ```rust - /// # use std::convert::Infallible; - /// # - /// # use async_trait::async_trait; - /// # use cucumber::{cli, WorldInit}; - /// # use structopt::StructOpt as _; - /// # - /// # #[derive(Debug, WorldInit)] - /// # struct MyWorld; - /// # - /// # #[async_trait(?Send)] - /// # impl cucumber::World for MyWorld { - /// # type Error = Infallible; - /// # - /// # async fn new() -> Result { - /// # Ok(Self) - /// # } - /// # } - /// # - /// # let fut = async { - /// let cli = cli::Opts::<_, _, _, cli::Empty>::from_args(); - /// // but something more meaningful :) ^ - /// // Work with cli.custom - /// - /// MyWorld::cucumber() - /// .run_and_exit_with_cli(cli, "tests/features/readme") - /// .await; - /// # }; - /// # - /// # futures::executor::block_on(fut); - /// ``` - /// - /// [`Failed`]: crate::event::Step::Failed - /// [`Feature`]: gherkin::Feature - /// [`Step`]: gherkin::Step - pub async fn run_and_exit_with_cli( - self, - cli: cli::Opts, - input: I, - ) where - Cli: StructOpt, - { - self.filter_run_and_exit_with_cli(cli, input, |_, _, _| true) - .await; - } - /// Runs [`Cucumber`] with [`Scenario`]s filter. /// /// [`Feature`]s sourced from a [`Parser`] are fed to a [`Runner`], which @@ -1255,97 +1284,4 @@ where .await .panic_with_diagnostic_message(); } - - /// Runs [`Cucumber`] with [`Scenario`]s filter and provided [`cli::Opts`]. - /// - /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` by providing [`StructOpt`] deriver as first generic - /// parameter in [`cli::Opts`]. - /// - /// # Panics - /// - /// If encountered errors while parsing [`Feature`]s or at least one - /// [`Step`] [`Failed`]. - /// - /// # Example - /// - /// Adjust [`Cucumber`] to run only [`Scenario`]s marked with `@cat` tag: - /// ```rust - /// # use std::convert::Infallible; - /// # - /// # use async_trait::async_trait; - /// # use cucumber::{WorldInit, cli}; - /// # use structopt::StructOpt as _; - /// # - /// # #[derive(Debug, WorldInit)] - /// # struct MyWorld; - /// # - /// # #[async_trait(?Send)] - /// # impl cucumber::World for MyWorld { - /// # type Error = Infallible; - /// # - /// # async fn new() -> Result { - /// # Ok(Self) - /// # } - /// # } - /// # - /// # let fut = async { - /// let cli = cli::Opts::<_, _, _, cli::Empty>::from_args(); - /// // but something more meaningful :) ^ - /// // Work with cli.custom - /// - /// MyWorld::cucumber() - /// .filter_run_and_exit_with_cli( - /// cli, - /// "tests/features/readme", - /// |_, _, sc| sc.tags.iter().any(|t| t == "cat"), - /// ) - /// .await; - /// # }; - /// # - /// # futures::executor::block_on(fut); - /// ``` - /// ```gherkin - /// Feature: Animal feature - /// - /// @cat - /// Scenario: If we feed a hungry cat it will no longer be hungry - /// Given a hungry cat - /// When I feed the cat - /// Then the cat is not hungry - /// - /// @dog - /// Scenario: If we feed a satiated dog it will not become hungry - /// Given a satiated dog - /// When I feed the dog - /// Then the dog is not hungry - /// ``` - /// - /// - /// [`Failed`]: crate::event::Step::Failed - /// [`Feature`]: gherkin::Feature - /// [`Scenario`]: gherkin::Scenario - /// [`Step`]: crate::Step - pub async fn filter_run_and_exit_with_cli( - self, - cli: cli::Opts, - input: I, - filter: Filter, - ) where - Cli: StructOpt, - Filter: Fn( - &gherkin::Feature, - Option<&gherkin::Rule>, - &gherkin::Scenario, - ) -> bool - + 'static, - { - self.filter_run_with_cli(cli, input, filter) - .await - .panic_with_diagnostic_message(); - } } diff --git a/tests/wait.rs b/tests/wait.rs index ec87e380..82ea28b6 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -1,23 +1,37 @@ use std::{convert::Infallible, panic::AssertUnwindSafe, time::Duration}; use async_trait::async_trait; -use cucumber::{given, then, when, WorldInit}; +use cucumber::{cli, given, then, when, WorldInit}; use futures::FutureExt as _; +use structopt::StructOpt; use tokio::time; +#[derive(StructOpt)] +struct Cli { + /// Time to wait in before and after hooks. + #[structopt( + long, + default_value = "10ms", + parse(try_from_str = humantime::parse_duration) + )] + time: Duration, +} + #[tokio::main] async fn main() { + let cli = cli::Opts::<_, _, _, Cli>::from_args(); + + let time = cli.custom.time; let res = World::cucumber() - .before(|_, _, _, w| { + .before(move |_, _, _, w| { async move { w.0 = 0; - time::sleep(Duration::from_millis(10)).await; + time::sleep(time).await; } .boxed_local() }) - .after(|_, _, _, _| { - time::sleep(Duration::from_millis(10)).boxed_local() - }) + .after(move |_, _, _, _| time::sleep(time).boxed_local()) + .with_cli(cli) .run_and_exit("tests/features/wait"); let err = AssertUnwindSafe(res) From c2d254c5c83979246cc3987efe32f68895a56db0 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 22 Oct 2021 12:57:01 +0300 Subject: [PATCH 19/27] Corrections --- CHANGELOG.md | 1 + book/src/Features.md | 4 +-- src/cucumber.rs | 70 +++++++++++++++++++++++++++++++++++++++----- src/writer/mod.rs | 41 -------------------------- 4 files changed, 66 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce202912..e3ead52d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [#137]: /../../pull/137 [#142]: /../../pull/142 [#143]: /../../pull/143 +[#144]: /../../pull/144 diff --git a/book/src/Features.md b/book/src/Features.md index 17ac42bd..dc67dee2 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -384,7 +384,7 @@ World::cucumber() ## CLI options -`Cucumber` provides several options that can be passed to on the command-line. +`Cucumber` provides several options that can be passed to the command-line. Pass the `--help` option to print out all the available configuration options: @@ -429,7 +429,7 @@ All `CLI` options are designed to be composable. For example all building-block traits have `CLI` associated type: [`Parser::Cli`](https://docs.rs/cucumber/*/cucumber/trait.Parser.html#associatedtype.Cli), [`Runner::Cli`](https://docs.rs/cucumber/*/cucumber/trait.Runner.html#associatedtype.Cli) and [`Writer::Cli`](https://docs.rs/cucumber/*/cucumber/trait.Writer.html#associatedtype.Cli). All of them are composed into a single `CLI`. -In case you want to add completely custom `CLI` options, check out [`Cucumber::run_and_exit_with_cli()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.run_and_exit_with_cli) method. +In case you want to add completely custom `CLI` options, check out [`Cucumber::with_cli()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.with_cli) method. diff --git a/src/cucumber.rs b/src/cucumber.rs index 74449b7a..b50b77da 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -702,10 +702,7 @@ where /// } /// /// let cli = cli::Opts::<_, _, _, Cli>::from_args(); - /// let time = cli - /// .custom - /// .before_time - /// .unwrap_or_default(); + /// let time = cli.custom.before_time.unwrap_or_default(); /// /// MyWorld::cucumber() /// .before(move |_, _, _, _| time::sleep(time).boxed_local()) @@ -734,6 +731,35 @@ where /// async data-autoplay="true" data-rows="14"> /// /// + /// Also now executing `--help` will output `--before-time` + /// + /// ```text + /// $ cargo test --test -- --help + /// cucumber 0.10.0 + /// Run the tests, pet a dog! + /// + /// USAGE: + /// cucumber [FLAGS] [OPTIONS] + /// + /// FLAGS: + /// -h, --help Prints help information + /// -V, --version Prints version information + /// --verbose Outputs Step's Doc String, if present + /// + /// OPTIONS: + /// -c, --colors Indicates, whether output should + /// be colored or not + /// [default: auto] + /// --before-time Time to wait in before hook + /// -f, --features `.feature` files glob pattern + /// --concurrent Number of concurrent scenarios + /// -n, --name Regex to filter scenarios with + /// [aliases: scenario-name] + /// -t, --tags Tag expression to filter + /// scenarios with + /// [aliases: scenario-tags] + /// ``` + /// /// [`Feature`]: gherkin::Feature pub fn with_cli( self, @@ -1280,8 +1306,38 @@ where ) -> bool + 'static, { - self.filter_run(input, filter) - .await - .panic_with_diagnostic_message(); + 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!( + "{} step{} failed", + failed_steps, + (failed_steps > 1).then(|| "s").unwrap_or_default(), + )); + } + + let parsing_errors = writer.parsing_errors(); + if parsing_errors > 0 { + msg.push(format!( + "{} parsing error{}", + parsing_errors, + (parsing_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + let hook_errors = writer.hook_errors(); + if hook_errors > 0 { + msg.push(format!( + "{} hook error{}", + hook_errors, + (hook_errors > 1).then(|| "s").unwrap_or_default(), + )); + } + + panic!("{}", msg.join(", ")); + } } } diff --git a/src/writer/mod.rs b/src/writer/mod.rs index c2874ede..3516c263 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -106,47 +106,6 @@ pub trait Failure: Writer { /// [`Scenario`]: gherkin::Scenario #[must_use] fn hook_errors(&self) -> usize; - - /// Panics with diagnostic message in case [`execution_has_failed`][1]. - /// - /// Default message looks like: - /// `1 step failed, 2 parsing errors, 3 hook errors`. - /// - /// [1]: Self::execution_has_failed() - fn panic_with_diagnostic_message(&self) { - if self.execution_has_failed() { - let mut msg = Vec::with_capacity(3); - - let failed_steps = self.failed_steps(); - if failed_steps > 0 { - msg.push(format!( - "{} step{} failed", - failed_steps, - (failed_steps > 1).then(|| "s").unwrap_or_default(), - )); - } - - let parsing_errors = self.parsing_errors(); - if parsing_errors > 0 { - msg.push(format!( - "{} parsing error{}", - parsing_errors, - (parsing_errors > 1).then(|| "s").unwrap_or_default(), - )); - } - - let hook_errors = self.hook_errors(); - if hook_errors > 0 { - msg.push(format!( - "{} hook error{}", - hook_errors, - (hook_errors > 1).then(|| "s").unwrap_or_default(), - )); - } - - panic!("{}", msg.join(", ")); - } - } } /// Extension of [`Writer`] allowing its normalization and summarization. From ff891a9cb4891f968022356e6c04691c93881847 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 22 Oct 2021 16:31:05 +0300 Subject: [PATCH 20/27] Pair DateTime with each event --- Cargo.toml | 1 + src/cli.rs | 4 +- src/cucumber.rs | 4 +- src/runner/basic.rs | 33 ++++-- src/runner/mod.rs | 7 +- src/writer/basic.rs | 2 + src/writer/fail_on_skipped.rs | 4 +- src/writer/mod.rs | 2 + src/writer/normalized.rs | 108 +++++++++++------- src/writer/repeat.rs | 12 +- src/writer/summarized.rs | 4 +- .../output/ambiguous_step.feature.out | 2 +- tests/output.rs | 2 + 13 files changed, 122 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0740a997..2a9af66e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ macros = ["cucumber-codegen", "inventory"] [dependencies] async-trait = "0.1.40" atty = "0.2.14" +chrono = "0.4" console = "0.15" derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false } either = "1.6" diff --git a/src/cli.rs b/src/cli.rs index 0f78c55f..1f58eebc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -107,6 +107,7 @@ wraps another [`Writer`]. ```rust # use async_trait::async_trait; +# use chrono::{DateTime, Utc}; # use cucumber::{ # cli, event, parser, ArbitraryWriter, FailureWriter, World, Writer, # }; @@ -131,11 +132,12 @@ where async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ) { // Some custom logic including `cli.left.custom_option`. - self.0.handle_event(ev, &cli.right).await; + self.0.handle_event(ev, at, &cli.right).await; } } diff --git a/src/cucumber.rs b/src/cucumber.rs index b50b77da..17d22ab4 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -907,8 +907,8 @@ where 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; + while let Some((ev, at)) = events_stream.next().await { + writer.handle_event(ev, at, &writer_cli).await; } writer } diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 6f67f4cb..51e369cb 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -23,6 +23,7 @@ use std::{ }, }; +use chrono::{DateTime, Utc}; use futures::{ channel::mpsc, future::{self, Either, LocalBoxFuture}, @@ -385,8 +386,10 @@ where { type Cli = Cli; - type EventStream = - LocalBoxStream<'static, parser::Result>>; + type EventStream = LocalBoxStream< + 'static, + (parser::Result>, DateTime), + >; fn run(self, features: S, cli: Cli) -> Self::EventStream where @@ -441,7 +444,10 @@ async fn insert_features( into: Features, features: S, which_scenario: F, - sender: mpsc::UnboundedSender>>, + sender: mpsc::UnboundedSender<( + parser::Result>, + DateTime, + )>, ) where S: Stream> + 'static, F: Fn( @@ -458,7 +464,7 @@ async fn insert_features( // If the receiver end is dropped, then no one listens for events // so we can just stop from here. Err(e) => { - if sender.unbounded_send(Err(e)).is_err() { + if sender.unbounded_send((Err(e), Utc::now())).is_err() { break; } } @@ -475,7 +481,10 @@ async fn execute( features: Features, max_concurrent_scenarios: Option, collection: step::Collection, - sender: mpsc::UnboundedSender>>, + sender: mpsc::UnboundedSender<( + parser::Result>, + DateTime, + )>, before_hook: Option, after_hook: Option, ) where @@ -583,7 +592,10 @@ struct Executor { /// Sender for notifying state of [`Feature`]s completion. /// /// [`Feature`]: gherkin::Feature - sender: mpsc::UnboundedSender>>, + sender: mpsc::UnboundedSender<( + parser::Result>, + DateTime, + )>, } impl Executor @@ -608,7 +620,10 @@ where collection: step::Collection, before_hook: Option, after_hook: Option, - sender: mpsc::UnboundedSender>>, + sender: mpsc::UnboundedSender<( + parser::Result>, + DateTime, + )>, ) -> Self { Self { features_scenarios_count: HashMap::new(), @@ -1097,7 +1112,7 @@ where fn send(&self, event: event::Cucumber) { // If the receiver end is dropped, then no one listens for events // so we can just ignore it. - drop(self.sender.unbounded_send(Ok(event))); + drop(self.sender.unbounded_send((Ok(event), Utc::now()))); } /// Notifies with the given [`Cucumber`] events. @@ -1107,7 +1122,7 @@ where for ev in events { // If the receiver end is dropped, then no one listens for events // so we can just stop from here. - if self.sender.unbounded_send(Ok(ev)).is_err() { + if self.sender.unbounded_send((Ok(ev), Utc::now())).is_err() { break; } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index c444e17e..7ec45ff8 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -16,6 +16,7 @@ pub mod basic; +use chrono::{DateTime, Utc}; use futures::Stream; use structopt::StructOptInternal; @@ -75,8 +76,10 @@ pub trait Runner { // details being a subject of instability. type Cli: StructOptInternal; - /// Output events [`Stream`]. - type EventStream: Stream>>; + /// Output events [`Stream`] paired with [`DateTime`] when they happen. + type EventStream: Stream< + Item = (parser::Result>, DateTime), + >; /// Executes the given [`Stream`] of [`Feature`]s transforming it into /// a [`Stream`] of executed [`Cucumber`] events. diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 1d41c3c7..79100b78 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -20,6 +20,7 @@ use std::{ }; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use console::Term; use itertools::Itertools as _; use regex::CaptureLocations; @@ -110,6 +111,7 @@ impl Writer for Basic { async fn handle_event( &mut self, ev: parser::Result>, + _: DateTime, cli: &Self::Cli, ) { use event::{Cucumber, Feature}; diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index d51bb420..86d63bd2 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -17,6 +17,7 @@ use std::sync::Arc; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use derive_more::Deref; use crate::{event, parser, ArbitraryWriter, FailureWriter, World, Writer}; @@ -64,6 +65,7 @@ where async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ) { use event::{ @@ -95,7 +97,7 @@ where _ => ev, }; - self.writer.handle_event(ev, cli).await; + self.writer.handle_event(ev, at, cli).await; } } diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 3516c263..3d7c8d1e 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -20,6 +20,7 @@ pub mod summarized; pub mod term; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use sealed::sealed; use structopt::StructOptInternal; @@ -63,6 +64,7 @@ pub trait Writer { async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ); } diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index d643f770..2b09ec50 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -13,6 +13,7 @@ use std::{hash::Hash, sync::Arc}; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use derive_more::Deref; use either::Either; use linked_hash_map::LinkedHashMap; @@ -53,7 +54,7 @@ impl Normalized { pub fn new(writer: Writer) -> Self { Self { writer, - queue: CucumberQueue::new(), + queue: CucumberQueue::new(Utc::now()), } } } @@ -65,6 +66,7 @@ impl> Writer for Normalized { async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ) { use event::{Cucumber, Feature, Rule}; @@ -73,28 +75,28 @@ impl> Writer for Normalized { // without any normalization. // This is done to avoid panic if this `Writer` happens to be wrapped // inside `writer::Repeat` or similar. - if self.queue.finished { - self.writer.handle_event(ev, cli).await; + if self.queue.is_finished() { + self.writer.handle_event(ev, at, cli).await; return; } match ev { res @ (Err(_) | Ok(Cucumber::Started)) => { - self.writer.handle_event(res, cli).await; + self.writer.handle_event(res, at, cli).await; } - Ok(Cucumber::Finished) => self.queue.finished(), + Ok(Cucumber::Finished) => self.queue.finished(at), Ok(Cucumber::Feature(f, ev)) => match ev { - Feature::Started => self.queue.new_feature(f), + Feature::Started => self.queue.new_feature(f, at), Feature::Scenario(s, ev) => { - self.queue.insert_scenario_event(&f, None, s, ev); + self.queue.insert_scenario_event(&f, None, s, ev, at); } - Feature::Finished => self.queue.feature_finished(&f), + Feature::Finished => self.queue.feature_finished(&f, at), Feature::Rule(r, ev) => match ev { - Rule::Started => self.queue.new_rule(&f, r), - Rule::Scenario(s, ev) => { - self.queue.insert_scenario_event(&f, Some(r), s, ev); + Rule::Started => self.queue.new_rule(&f, r, at), + Rule::Scenario(s, e) => { + self.queue.insert_scenario_event(&f, Some(r), s, e, at); } - Rule::Finished => self.queue.rule_finished(&f, r), + Rule::Finished => self.queue.rule_finished(&f, r, at), }, }, } @@ -105,8 +107,10 @@ impl> Writer for Normalized { self.queue.remove(&feature_to_remove); } - if self.queue.is_finished() { - self.writer.handle_event(Ok(Cucumber::Finished), cli).await; + if let Some(at) = self.queue.finished { + self.writer + .handle_event(Ok(Cucumber::Finished), at, cli) + .await; } } } @@ -161,17 +165,27 @@ struct Queue { /// Underlying FIFO queue of values. queue: LinkedHashMap, - /// Indicator whether this [`Queue`] finished emitting values. - finished: bool, + /// [`DateTime`] when this [`Queue`] was created. + /// + /// This value corresponds to the [`DateTime`] of the `Started` event for + /// item, this [`Queue`] is representing. + started: DateTime, + + /// [`DateTime`] when [`Queue`] finished emitting values. + /// + /// This value corresponds to the [`DateTime`] of the `Finished` event for + /// item, this [`Queue`] is representing. + finished: Option>, } impl Queue { /// Creates a new empty normalization [`Queue`]. - fn new() -> Self { + fn new(started: DateTime) -> Self { Self { started_emitted: false, queue: LinkedHashMap::new(), - finished: false, + started, + finished: None, } } @@ -186,13 +200,13 @@ impl Queue { } /// Marks this [`Queue`] as finished. - fn finished(&mut self) { - self.finished = true; + fn finished(&mut self, at: DateTime) { + self.finished = Some(at); } /// Checks whether this [`Queue`] has been finished. fn is_finished(&self) -> bool { - self.finished + self.finished.is_some() } /// Removes the given `key` from this [`Queue`]. @@ -266,8 +280,8 @@ impl CucumberQueue { /// Inserts a new [`Feature`] on [`event::Feature::Started`]. /// /// [`Feature`]: gherkin::Feature - fn new_feature(&mut self, feat: Arc) { - drop(self.queue.insert(feat, FeatureQueue::new())); + fn new_feature(&mut self, feat: Arc, at: DateTime) { + drop(self.queue.insert(feat, FeatureQueue::new(at))); } /// Marks a [`Feature`] as finished on [`event::Feature::Finished`]. @@ -276,21 +290,26 @@ impl CucumberQueue { /// [`Feature`]s holding the output. /// /// [`Feature`]: gherkin::Feature - fn feature_finished(&mut self, feat: &gherkin::Feature) { + fn feature_finished(&mut self, feat: &gherkin::Feature, at: DateTime) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .finished(); + .finished(at); } /// Inserts a new [`Rule`] on [`event::Rule::Started`]. /// /// [`Rule`]: gherkin::Feature - fn new_rule(&mut self, feat: &gherkin::Feature, rule: Arc) { + fn new_rule( + &mut self, + feat: &gherkin::Feature, + rule: Arc, + at: DateTime, + ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .new_rule(rule); + .new_rule(rule, at); } /// Marks a [`Rule`] as finished on [`event::Rule::Finished`]. @@ -303,11 +322,12 @@ impl CucumberQueue { &mut self, feat: &gherkin::Feature, rule: Arc, + at: DateTime, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .rule_finished(rule); + .rule_finished(rule, at); } /// Inserts a new [`event::Scenario::Started`]. @@ -317,11 +337,12 @@ impl CucumberQueue { rule: Option>, scenario: Arc, event: event::Scenario, + at: DateTime, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .insert_scenario_event(rule, scenario, event); + .insert_scenario_event(rule, scenario, event, at); } } @@ -349,6 +370,7 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { writer .handle_event( Ok(event::Cucumber::feature_started(Arc::clone(&f))), + events.started, cli, ) .await; @@ -361,10 +383,11 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { events.remove(&scenario_or_rule_to_remove); } - if events.is_finished() { + if let Some(at) = events.finished { writer .handle_event( Ok(event::Cucumber::feature_finished(Arc::clone(&f))), + at, cli, ) .await; @@ -407,20 +430,20 @@ impl FeatureQueue { /// Inserts a new [`Rule`]. /// /// [`Rule`]: gherkin::Rule - fn new_rule(&mut self, rule: Arc) { + fn new_rule(&mut self, rule: Arc, at: DateTime) { drop( self.queue - .insert(Either::Left(rule), Either::Left(RulesQueue::new())), + .insert(Either::Left(rule), Either::Left(RulesQueue::new(at))), ); } /// Marks a [`Rule`] as finished on [`event::Rule::Finished`]. /// /// [`Rule`]: gherkin::Rule - fn rule_finished(&mut self, rule: Arc) { + fn rule_finished(&mut self, rule: Arc, at: DateTime) { match self.queue.get_mut(&Either::Left(rule)).unwrap() { Either::Left(ev) => { - ev.finished(); + ev.finished(at); } Either::Right(_) => unreachable!(), } @@ -434,6 +457,7 @@ impl FeatureQueue { rule: Option>, scenario: Arc, ev: event::Scenario, + at: DateTime, ) { if let Some(rule) = rule { match self @@ -446,7 +470,7 @@ impl FeatureQueue { .entry(scenario) .or_insert_with(ScenariosQueue::new) .0 - .push(ev), + .push((ev, at)), Either::Right(_) => unreachable!(), } } else { @@ -455,7 +479,7 @@ impl FeatureQueue { .entry(Either::Right(scenario)) .or_insert_with(|| Either::Right(ScenariosQueue::new())) { - Either::Right(events) => events.0.push(ev), + Either::Right(events) => events.0.push((ev, at)), Either::Left(_) => unreachable!(), } } @@ -530,6 +554,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { Arc::clone(&feature), Arc::clone(&rule), )), + self.started, cli, ) .await; @@ -551,13 +576,14 @@ impl<'me, World> Emitter for &'me mut RulesQueue { } } - if self.is_finished() { + if let Some(at) = self.finished { writer .handle_event( Ok(event::Cucumber::rule_finished( feature, Arc::clone(&rule), )), + at, cli, ) .await; @@ -572,7 +598,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { /// /// [`Scenario`]: gherkin::Scenario #[derive(Debug)] -struct ScenariosQueue(Vec>); +struct ScenariosQueue(Vec<(event::Scenario, DateTime)>); impl ScenariosQueue { /// Creates a new [`ScenariosQueue`]. @@ -583,7 +609,7 @@ impl ScenariosQueue { #[async_trait(?Send)] impl Emitter for &mut ScenariosQueue { - type Current = event::Scenario; + type Current = (event::Scenario, DateTime); type Emitted = Arc; type EmittedPath = ( Arc, @@ -601,7 +627,7 @@ impl Emitter for &mut ScenariosQueue { writer: &mut W, cli: &W::Cli, ) -> Option { - while let Some(ev) = self.current_item() { + while let Some((ev, at)) = self.current_item() { let should_be_removed = matches!(ev, event::Scenario::Finished); let ev = event::Cucumber::scenario( @@ -610,7 +636,7 @@ impl Emitter for &mut ScenariosQueue { Arc::clone(&scenario), ev, ); - writer.handle_event(Ok(ev), cli).await; + writer.handle_event(Ok(ev), at, cli).await; if should_be_removed { return Some(Arc::clone(&scenario)); diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 947ac3d5..9f5ef6d0 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -13,6 +13,7 @@ use std::mem; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use derive_more::Deref; use crate::{event, parser, ArbitraryWriter, FailureWriter, World, Writer}; @@ -35,7 +36,7 @@ pub struct Repeat> { filter: F, /// Buffer of collected events for re-outputting. - events: Vec>>, + events: Vec<(parser::Result>, DateTime)>, } /// Alias for a [`fn`] predicate deciding whether an event should be @@ -54,19 +55,20 @@ where async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ) { if (self.filter)(&ev) { - self.events.push(ev.clone()); + self.events.push((ev.clone(), at)); } let is_finished = matches!(ev, Ok(event::Cucumber::Finished)); - self.writer.handle_event(ev, cli).await; + self.writer.handle_event(ev, at, cli).await; if is_finished { - for ev in mem::take(&mut self.events) { - self.writer.handle_event(ev, cli).await; + for (ev, at) in mem::take(&mut self.events) { + self.writer.handle_event(ev, at, cli).await; } } } diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index e7c543e3..707fbdc2 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -13,6 +13,7 @@ use std::{array, borrow::Cow, collections::HashMap, sync::Arc}; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use derive_more::Deref; use itertools::Itertools as _; @@ -146,6 +147,7 @@ where async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ) { use event::{Cucumber, Feature, Rule}; @@ -168,7 +170,7 @@ where Ok(Cucumber::Started) => {} }; - self.writer.handle_event(ev, cli).await; + self.writer.handle_event(ev, at, cli).await; if finished { self.writer.write(Styles::new().summary(self)).await; diff --git a/tests/features/output/ambiguous_step.feature.out b/tests/features/output/ambiguous_step.feature.out index 8e9510b5..38667cb2 100644 --- a/tests/features/output/ambiguous_step.feature.out +++ b/tests/features/output/ambiguous_step.feature.out @@ -2,7 +2,7 @@ Started Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Started) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Started)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Started))) -Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 14, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 22, column: 1 }))] }))))) +Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 15, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 23, column: 1 }))] }))))) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Finished)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Finished) Finished diff --git a/tests/output.rs b/tests/output.rs index bd54adce..dc057fb3 100644 --- a/tests/output.rs +++ b/tests/output.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, cmp::Ordering, convert::Infallible, fmt::Debug}; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use cucumber::{ cli, event, given, parser, step, then, when, WorldInit, Writer, }; @@ -41,6 +42,7 @@ impl Writer for DebugWriter { async fn handle_event( &mut self, ev: parser::Result>, + _: DateTime, _: &Self::Cli, ) { use event::{Cucumber, Feature, Rule, Scenario, Step, StepError}; From 598d0bc6a4822840a22db686bf4792e230eb58f0 Mon Sep 17 00:00:00 2001 From: ilslv Date: Fri, 22 Oct 2021 16:35:55 +0300 Subject: [PATCH 21/27] Correction --- src/runner/mod.rs | 2 +- src/writer/normalized.rs | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 7ec45ff8..d0c12e23 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -76,7 +76,7 @@ pub trait Runner { // details being a subject of instability. type Cli: StructOptInternal; - /// Output events [`Stream`] paired with [`DateTime`] when they happen. + /// Output events [`Stream`] paired with [`DateTime`] when they happened. type EventStream: Stream< Item = (parser::Result>, DateTime), >; diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 2b09ec50..c91f090c 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -107,7 +107,7 @@ impl> Writer for Normalized { self.queue.remove(&feature_to_remove); } - if let Some(at) = self.queue.finished { + if let Some(at) = self.queue.finished_at { self.writer .handle_event(Ok(Cucumber::Finished), at, cli) .await; @@ -159,9 +159,6 @@ where /// [`next()`]: std::iter::Iterator::next() #[derive(Debug)] struct Queue { - /// Indicator whether this [`Queue`] started emitting values. - started_emitted: bool, - /// Underlying FIFO queue of values. queue: LinkedHashMap, @@ -169,23 +166,26 @@ struct Queue { /// /// This value corresponds to the [`DateTime`] of the `Started` event for /// item, this [`Queue`] is representing. - started: DateTime, + started_at: DateTime, + + /// Indicator whether this [`Queue`] started emitting values. + started_emitted: bool, /// [`DateTime`] when [`Queue`] finished emitting values. /// /// This value corresponds to the [`DateTime`] of the `Finished` event for /// item, this [`Queue`] is representing. - finished: Option>, + finished_at: Option>, } impl Queue { /// Creates a new empty normalization [`Queue`]. fn new(started: DateTime) -> Self { Self { - started_emitted: false, queue: LinkedHashMap::new(), - started, - finished: None, + started_at: started, + started_emitted: false, + finished_at: None, } } @@ -201,12 +201,12 @@ impl Queue { /// Marks this [`Queue`] as finished. fn finished(&mut self, at: DateTime) { - self.finished = Some(at); + self.finished_at = Some(at); } /// Checks whether this [`Queue`] has been finished. fn is_finished(&self) -> bool { - self.finished.is_some() + self.finished_at.is_some() } /// Removes the given `key` from this [`Queue`]. @@ -370,7 +370,7 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { writer .handle_event( Ok(event::Cucumber::feature_started(Arc::clone(&f))), - events.started, + events.started_at, cli, ) .await; @@ -383,7 +383,7 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { events.remove(&scenario_or_rule_to_remove); } - if let Some(at) = events.finished { + if let Some(at) = events.finished_at { writer .handle_event( Ok(event::Cucumber::feature_finished(Arc::clone(&f))), @@ -554,7 +554,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { Arc::clone(&feature), Arc::clone(&rule), )), - self.started, + self.started_at, cli, ) .await; @@ -576,7 +576,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { } } - if let Some(at) = self.finished { + if let Some(at) = self.finished_at { writer .handle_event( Ok(event::Cucumber::rule_finished( From be20764525d548619f429575c90255be6b401fa2 Mon Sep 17 00:00:00 2001 From: ilslv Date: Mon, 25 Oct 2021 07:53:42 +0300 Subject: [PATCH 22/27] Fix chrono at 0.4.19 to avoid problems with minimal-versions --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2a9af66e..846d261c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ macros = ["cucumber-codegen", "inventory"] [dependencies] async-trait = "0.1.40" atty = "0.2.14" -chrono = "0.4" +chrono = "0.4.19" console = "0.15" derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false } either = "1.6" From aab2e2701722ab230cfcbc01f5dc99bb2c3a2aa3 Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 26 Oct 2021 09:31:16 +0300 Subject: [PATCH 23/27] Refactor `(Event, DateTime)` into `DateTimed` --- src/cli.rs | 4 +- src/cucumber.rs | 4 +- src/event.rs | 28 +++++++++++++ src/runner/basic.rs | 42 ++++++++----------- src/runner/mod.rs | 10 +++-- src/writer/normalized.rs | 15 ++++--- src/writer/repeat.rs | 11 +++-- .../output/ambiguous_step.feature.out | 2 +- tests/output.rs | 2 + 9 files changed, 76 insertions(+), 42 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index bab04650..94d77729 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -178,6 +178,7 @@ This struct is especially useful, when implementing custom [`Writer`] wrapping another one: ```rust # use async_trait::async_trait; +# use chrono::{DateTime, Utc}; # use cucumber::{ # cli, event, parser, ArbitraryWriter, FailureWriter, World, Writer, # }; @@ -202,11 +203,12 @@ where async fn handle_event( &mut self, ev: parser::Result>, + at: DateTime, cli: &Self::Cli, ) { // Some custom logic including `cli.left.custom_option`. // ... - self.0.handle_event(ev, &cli.right).await; + self.0.handle_event(ev, at, &cli.right).await; } } diff --git a/src/cucumber.rs b/src/cucumber.rs index 6146bd31..b1c527d0 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -882,8 +882,8 @@ where let events_stream = runner.run(filtered, runner_cli); futures::pin_mut!(events_stream); - while let Some((ev, at)) = events_stream.next().await { - writer.handle_event(ev, at, &writer_cli).await; + while let Some(ev) = events_stream.next().await { + writer.handle_event(ev.inner, ev.at, &writer_cli).await; } writer } diff --git a/src/event.rs b/src/event.rs index 60aecb1d..346aee9c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -21,6 +21,7 @@ use std::{any::Any, fmt, sync::Arc}; +use chrono::{DateTime, Utc}; use derive_more::{Display, Error, From}; use crate::{step, writer::basic::coerce_error}; @@ -30,6 +31,33 @@ use crate::{step, writer::basic::coerce_error}; /// [`catch_unwind()`]: std::panic::catch_unwind() pub type Info = Arc; +/// Value paired with [`DateTime`]. +#[derive(Clone, Copy, Debug)] +pub struct DateTimed { + /// Value itself. + pub inner: T, + + /// [`DateTime`] paired with the value. + pub at: DateTime, +} + +impl DateTimed { + /// Creates a new [`DateTimed`] with [`Utc::now()`] [`DateTime`]. + #[must_use] + pub fn now(inner: T) -> Self { + Self { + inner, + at: Utc::now(), + } + } + + /// Creates a new [`DateTimed`] with provided [`DateTime`]. + #[must_use] + pub const fn at(inner: T, at: DateTime) -> Self { + Self { inner, at } + } +} + /// Top-level [Cucumber] run event. /// /// [Cucumber]: https://cucumber.io diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 582ee374..973c8651 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -23,7 +23,6 @@ use std::{ }, }; -use chrono::{DateTime, Utc}; use futures::{ channel::mpsc, future::{self, Either, LocalBoxFuture}, @@ -38,7 +37,7 @@ use regex::{CaptureLocations, Regex}; use structopt::StructOpt; use crate::{ - event::{self, HookType, Info}, + event::{self, DateTimed, HookType, Info}, feature::Ext as _, parser, step, Runner, Step, World, }; @@ -387,10 +386,8 @@ where { type Cli = Cli; - type EventStream = LocalBoxStream< - 'static, - (parser::Result>, DateTime), - >; + type EventStream = + LocalBoxStream<'static, DateTimed>>>; fn run(self, features: S, cli: Cli) -> Self::EventStream where @@ -445,10 +442,9 @@ async fn insert_features( into: Features, features: S, which_scenario: F, - sender: mpsc::UnboundedSender<( - parser::Result>, - DateTime, - )>, + sender: mpsc::UnboundedSender< + DateTimed>>, + >, ) where S: Stream> + 'static, F: Fn( @@ -465,7 +461,7 @@ async fn insert_features( // If the receiver end is dropped, then no one listens for events // so we can just stop from here. Err(e) => { - if sender.unbounded_send((Err(e), Utc::now())).is_err() { + if sender.unbounded_send(DateTimed::now(Err(e))).is_err() { break; } } @@ -482,10 +478,9 @@ async fn execute( features: Features, max_concurrent_scenarios: Option, collection: step::Collection, - sender: mpsc::UnboundedSender<( - parser::Result>, - DateTime, - )>, + sender: mpsc::UnboundedSender< + DateTimed>>, + >, before_hook: Option, after_hook: Option, ) where @@ -593,10 +588,8 @@ struct Executor { /// Sender for notifying state of [`Feature`]s completion. /// /// [`Feature`]: gherkin::Feature - sender: mpsc::UnboundedSender<( - parser::Result>, - DateTime, - )>, + sender: + mpsc::UnboundedSender>>>, } impl Executor @@ -621,10 +614,9 @@ where collection: step::Collection, before_hook: Option, after_hook: Option, - sender: mpsc::UnboundedSender<( - parser::Result>, - DateTime, - )>, + sender: mpsc::UnboundedSender< + DateTimed>>, + >, ) -> Self { Self { features_scenarios_count: HashMap::new(), @@ -1113,7 +1105,7 @@ where fn send(&self, event: event::Cucumber) { // If the receiver end is dropped, then no one listens for events // so we can just ignore it. - drop(self.sender.unbounded_send((Ok(event), Utc::now()))); + drop(self.sender.unbounded_send(DateTimed::now(Ok(event)))); } /// Notifies with the given [`Cucumber`] events. @@ -1123,7 +1115,7 @@ where for ev in events { // If the receiver end is dropped, then no one listens for events // so we can just stop from here. - if self.sender.unbounded_send((Ok(ev), Utc::now())).is_err() { + if self.sender.unbounded_send(DateTimed::now(Ok(ev))).is_err() { break; } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index ed9db969..9374ee75 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -16,11 +16,13 @@ pub mod basic; -use chrono::{DateTime, Utc}; use futures::Stream; use structopt::StructOptInternal; -use crate::{event, parser}; +use crate::{ + event::{self, DateTimed}, + parser, +}; #[doc(inline)] pub use self::basic::{Basic, ScenarioType}; @@ -77,8 +79,10 @@ pub trait Runner { type Cli: StructOptInternal; /// Output events [`Stream`] paired with a [`DateTime`] when they happened. + /// + /// [`DateTime`]: chrono::DateTime type EventStream: Stream< - Item = (parser::Result>, DateTime), + Item = DateTimed>>, >; /// Executes the given [`Stream`] of [`Feature`]s transforming it into diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index c91f090c..9ce7c85a 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -18,7 +18,10 @@ use derive_more::Deref; use either::Either; use linked_hash_map::LinkedHashMap; -use crate::{event, parser, ArbitraryWriter, FailureWriter, World, Writer}; +use crate::{ + event::{self, DateTimed}, + parser, ArbitraryWriter, FailureWriter, World, Writer, +}; /// Wrapper for a [`Writer`] implementation for outputting events corresponding /// to _order guarantees_ from the [`Runner`] in a normalized readable order. @@ -470,7 +473,7 @@ impl FeatureQueue { .entry(scenario) .or_insert_with(ScenariosQueue::new) .0 - .push((ev, at)), + .push(DateTimed::at(ev, at)), Either::Right(_) => unreachable!(), } } else { @@ -479,7 +482,7 @@ impl FeatureQueue { .entry(Either::Right(scenario)) .or_insert_with(|| Either::Right(ScenariosQueue::new())) { - Either::Right(events) => events.0.push((ev, at)), + Either::Right(events) => events.0.push(DateTimed::at(ev, at)), Either::Left(_) => unreachable!(), } } @@ -598,7 +601,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { /// /// [`Scenario`]: gherkin::Scenario #[derive(Debug)] -struct ScenariosQueue(Vec<(event::Scenario, DateTime)>); +struct ScenariosQueue(Vec>>); impl ScenariosQueue { /// Creates a new [`ScenariosQueue`]. @@ -609,7 +612,7 @@ impl ScenariosQueue { #[async_trait(?Send)] impl Emitter for &mut ScenariosQueue { - type Current = (event::Scenario, DateTime); + type Current = DateTimed>; type Emitted = Arc; type EmittedPath = ( Arc, @@ -627,7 +630,7 @@ impl Emitter for &mut ScenariosQueue { writer: &mut W, cli: &W::Cli, ) -> Option { - while let Some((ev, at)) = self.current_item() { + while let Some(DateTimed { inner: ev, at }) = self.current_item() { let should_be_removed = matches!(ev, event::Scenario::Finished); let ev = event::Cucumber::scenario( diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 9f5ef6d0..3bd92f4b 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -16,7 +16,10 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use derive_more::Deref; -use crate::{event, parser, ArbitraryWriter, FailureWriter, World, Writer}; +use crate::{ + event::{self, DateTimed}, + parser, ArbitraryWriter, FailureWriter, World, Writer, +}; /// Wrapper for a [`Writer`] implementation for re-outputting events at the end /// of an output, based on a filter predicated. @@ -36,7 +39,7 @@ pub struct Repeat> { filter: F, /// Buffer of collected events for re-outputting. - events: Vec<(parser::Result>, DateTime)>, + events: Vec>>>, } /// Alias for a [`fn`] predicate deciding whether an event should be @@ -59,7 +62,7 @@ where cli: &Self::Cli, ) { if (self.filter)(&ev) { - self.events.push((ev.clone(), at)); + self.events.push(DateTimed::at(ev.clone(), at)); } let is_finished = matches!(ev, Ok(event::Cucumber::Finished)); @@ -67,7 +70,7 @@ where self.writer.handle_event(ev, at, cli).await; if is_finished { - for (ev, at) in mem::take(&mut self.events) { + for DateTimed { inner: ev, at } in mem::take(&mut self.events) { self.writer.handle_event(ev, at, cli).await; } } diff --git a/tests/features/output/ambiguous_step.feature.out b/tests/features/output/ambiguous_step.feature.out index 8e9510b5..38667cb2 100644 --- a/tests/features/output/ambiguous_step.feature.out +++ b/tests/features/output/ambiguous_step.feature.out @@ -2,7 +2,7 @@ Started Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Started) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Started)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Started))) -Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 14, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 22, column: 1 }))] }))))) +Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 15, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 23, column: 1 }))] }))))) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Finished)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Finished) Finished diff --git a/tests/output.rs b/tests/output.rs index dc057fb3..b6330762 100644 --- a/tests/output.rs +++ b/tests/output.rs @@ -27,6 +27,7 @@ fn ambiguous(_w: &mut World) {} impl cucumber::World for World { type Error = Infallible; + #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn new() -> Result { Ok(World::default()) } @@ -39,6 +40,7 @@ struct DebugWriter(String); impl Writer for DebugWriter { type Cli = cli::Empty; + #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn handle_event( &mut self, ev: parser::Result>, From 0f4d2ea3e2d6e3e4df43ebdac0f23bf6130afeae Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 26 Oct 2021 16:44:57 +0300 Subject: [PATCH 24/27] Wrap `event::Cucumber` in `Event` --- Cargo.toml | 4 +- src/cli.rs | 8 +- src/cucumber.rs | 11 +- src/event.rs | 69 ++++-- src/lib.rs | 1 + src/runner/basic.rs | 25 +-- src/runner/mod.rs | 11 +- src/writer/basic.rs | 8 +- src/writer/fail_on_skipped.rs | 48 +++-- src/writer/mod.rs | 10 +- src/writer/normalized.rs | 203 ++++++++++-------- src/writer/repeat.rs | 33 +-- src/writer/summarized.rs | 13 +- .../output/ambiguous_step.feature.out | 2 +- tests/output.rs | 8 +- 15 files changed, 252 insertions(+), 202 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f94c9f38..41f8d154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,13 +28,13 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["macros"] macros = ["cucumber-codegen", "inventory"] +event-time = [] [dependencies] async-trait = "0.1.40" atty = "0.2.14" -chrono = "0.4.19" console = "0.15" -derive_more = { version = "0.99.16", features = ["deref", "deref_mut", "display", "error", "from"], default_features = false } +derive_more = { version = "0.99.16", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"], default_features = false } either = "1.6" futures = "0.3.17" gherkin = { package = "gherkin_rust", version = "0.10" } diff --git a/src/cli.rs b/src/cli.rs index 94d77729..af66cb4c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -178,9 +178,8 @@ This struct is especially useful, when implementing custom [`Writer`] wrapping another one: ```rust # use async_trait::async_trait; -# use chrono::{DateTime, Utc}; # use cucumber::{ -# cli, event, parser, ArbitraryWriter, FailureWriter, World, Writer, +# cli, event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, # }; # use structopt::StructOpt; # @@ -202,13 +201,12 @@ where async fn handle_event( &mut self, - ev: parser::Result>, - at: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ) { // Some custom logic including `cli.left.custom_option`. // ... - self.0.handle_event(ev, at, &cli.right).await; + self.0.handle_event(ev, &cli.right).await; } } diff --git a/src/cucumber.rs b/src/cucumber.rs index b1c527d0..9b0d3742 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -25,8 +25,9 @@ use regex::Regex; use structopt::{StructOpt, StructOptInternal}; use crate::{ - cli, event, parser, runner, step, tag::Ext as _, writer, FailureWriter, - Parser, Runner, ScenarioType, Step, World, Writer, WriterExt as _, + cli, event, parser, runner, step, tag::Ext as _, writer, Event, + FailureWriter, Parser, Runner, ScenarioType, Step, World, Writer, + WriterExt as _, }; /// Top-level [Cucumber] executor. @@ -387,7 +388,7 @@ where /// use cucumber::event::{Cucumber, Feature, Rule, Scenario, Step}; /// /// matches!( - /// ev, + /// ev.as_ref().map(AsRef::as_ref), /// Ok(Cucumber::Feature( /// _, /// Feature::Rule( @@ -434,7 +435,7 @@ where filter: F, ) -> Cucumber, Cli> where - F: Fn(&parser::Result>) -> bool, + F: Fn(&parser::Result>>) -> bool, { Cucumber { parser: self.parser, @@ -883,7 +884,7 @@ where 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.inner, ev.at, &writer_cli).await; + writer.handle_event(ev, &writer_cli).await; } writer } diff --git a/src/event.rs b/src/event.rs index 346aee9c..64e0f022 100644 --- a/src/event.rs +++ b/src/event.rs @@ -21,8 +21,10 @@ use std::{any::Any, fmt, sync::Arc}; -use chrono::{DateTime, Utc}; -use derive_more::{Display, Error, From}; +#[cfg(feature = "event-time")] +use std::time::SystemTime; + +use derive_more::{AsRef, Display, Error, From}; use crate::{step, writer::basic::coerce_error}; @@ -31,30 +33,65 @@ use crate::{step, writer::basic::coerce_error}; /// [`catch_unwind()`]: std::panic::catch_unwind() pub type Info = Arc; -/// Value paired with [`DateTime`]. -#[derive(Clone, Copy, Debug)] -pub struct DateTimed { - /// Value itself. +/// [`Cucumber`] event paired with additional metadata. +/// +/// Metadata is configured with cargo features. This is done mainly to give us +/// ability to add additional fields without introducing breaking changes. +#[derive(AsRef, Clone, Debug)] +pub struct Event { + /// [`SystemTime`] when [`Event`] happened. + #[cfg(feature = "event-time")] + pub at: SystemTime, + + /// Inner value. + #[as_ref] pub inner: T, - - /// [`DateTime`] paired with the value. - pub at: DateTime, } -impl DateTimed { - /// Creates a new [`DateTimed`] with [`Utc::now()`] [`DateTime`]. +impl Event { + /// Creates a new [`Event`]. + // False positive: SystemTime::now() isn't const + #[allow(clippy::missing_const_for_fn)] #[must_use] - pub fn now(inner: T) -> Self { + pub fn new(inner: T) -> Self { Self { + #[cfg(feature = "event-time")] + at: SystemTime::now(), inner, - at: Utc::now(), } } - /// Creates a new [`DateTimed`] with provided [`DateTime`]. + /// Returns [`Event::inner`]. + // False positive: `constant functions cannot evaluate destructors` + #[allow(clippy::missing_const_for_fn)] #[must_use] - pub const fn at(inner: T, at: DateTime) -> Self { - Self { inner, at } + pub fn into_inner(self) -> T { + self.inner + } + + /// Replaces [`Event::inner`] with `()`, returning the old one. + pub fn take(self) -> (T, Event<()>) { + self.replace(()) + } + + /// Replaces [`Event::inner`] with `value`, dropping the old one. + #[must_use] + pub fn insert(self, value: V) -> Event { + self.replace(value).1 + } + + /// Replaces [`Event::inner`] with `value`, returning the old one. + // False positive: `constant functions cannot evaluate destructors` + #[allow(clippy::missing_const_for_fn)] + pub fn replace(self, value: V) -> (T, Event) { + ( + self.inner, + Event { + inner: value, + #[cfg(feature = "event-time")] + at: self.at, + }, + ) } } diff --git a/src/lib.rs b/src/lib.rs index 22e5219e..47f4e146 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,7 @@ pub use cucumber_codegen::{given, then, when, WorldInit}; #[doc(inline)] pub use self::{ cucumber::Cucumber, + event::Event, parser::Parser, runner::{Runner, ScenarioType}, step::Step, diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 973c8651..38097abf 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -37,9 +37,9 @@ use regex::{CaptureLocations, Regex}; use structopt::StructOpt; use crate::{ - event::{self, DateTimed, HookType, Info}, + event::{self, HookType, Info}, feature::Ext as _, - parser, step, Runner, Step, World, + parser, step, Event, Runner, Step, World, }; // Workaround for overwritten doc-comments. @@ -387,7 +387,7 @@ where type Cli = Cli; type EventStream = - LocalBoxStream<'static, DateTimed>>>; + LocalBoxStream<'static, parser::Result>>>; fn run(self, features: S, cli: Cli) -> Self::EventStream where @@ -442,9 +442,7 @@ async fn insert_features( into: Features, features: S, which_scenario: F, - sender: mpsc::UnboundedSender< - DateTimed>>, - >, + sender: mpsc::UnboundedSender>>>, ) where S: Stream> + 'static, F: Fn( @@ -461,7 +459,7 @@ async fn insert_features( // If the receiver end is dropped, then no one listens for events // so we can just stop from here. Err(e) => { - if sender.unbounded_send(DateTimed::now(Err(e))).is_err() { + if sender.unbounded_send(Err(e)).is_err() { break; } } @@ -478,9 +476,7 @@ async fn execute( features: Features, max_concurrent_scenarios: Option, collection: step::Collection, - sender: mpsc::UnboundedSender< - DateTimed>>, - >, + sender: mpsc::UnboundedSender>>>, before_hook: Option, after_hook: Option, ) where @@ -588,8 +584,7 @@ struct Executor { /// Sender for notifying state of [`Feature`]s completion. /// /// [`Feature`]: gherkin::Feature - sender: - mpsc::UnboundedSender>>>, + sender: mpsc::UnboundedSender>>>, } impl Executor @@ -615,7 +610,7 @@ where before_hook: Option, after_hook: Option, sender: mpsc::UnboundedSender< - DateTimed>>, + parser::Result>>, >, ) -> Self { Self { @@ -1105,7 +1100,7 @@ where fn send(&self, event: event::Cucumber) { // If the receiver end is dropped, then no one listens for events // so we can just ignore it. - drop(self.sender.unbounded_send(DateTimed::now(Ok(event)))); + drop(self.sender.unbounded_send(Ok(Event::new(event)))); } /// Notifies with the given [`Cucumber`] events. @@ -1115,7 +1110,7 @@ where for ev in events { // If the receiver end is dropped, then no one listens for events // so we can just stop from here. - if self.sender.unbounded_send(DateTimed::now(Ok(ev))).is_err() { + if self.sender.unbounded_send(Ok(Event::new(ev))).is_err() { break; } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 9374ee75..1c5f0983 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -19,10 +19,7 @@ pub mod basic; use futures::Stream; use structopt::StructOptInternal; -use crate::{ - event::{self, DateTimed}, - parser, -}; +use crate::{event, parser, Event}; #[doc(inline)] pub use self::basic::{Basic, ScenarioType}; @@ -78,11 +75,9 @@ pub trait Runner { // details being a subject of instability. type Cli: StructOptInternal; - /// Output events [`Stream`] paired with a [`DateTime`] when they happened. - /// - /// [`DateTime`]: chrono::DateTime + /// Output events [`Stream`]. type EventStream: Stream< - Item = DateTimed>>, + Item = parser::Result>>, >; /// Executes the given [`Stream`] of [`Feature`]s transforming it into diff --git a/src/writer/basic.rs b/src/writer/basic.rs index ffd4267c..91b24cdb 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -20,7 +20,6 @@ use std::{ }; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use console::Term; use itertools::Itertools as _; use regex::CaptureLocations; @@ -30,7 +29,7 @@ use crate::{ event::{self, Info}, parser, writer::term::Styles, - ArbitraryWriter, World, Writer, + ArbitraryWriter, Event, World, Writer, }; // Workaround for overwritten doc-comments. @@ -106,8 +105,7 @@ impl Writer for Basic { #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn handle_event( &mut self, - ev: parser::Result>, - _: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ) { use event::{Cucumber, Feature}; @@ -118,7 +116,7 @@ impl Writer for Basic { Coloring::Auto => {} }; - match ev { + match ev.map(Event::into_inner) { Err(err) => self.parsing_failed(&err), Ok(Cucumber::Started | Cucumber::Finished) => Ok(()), Ok(Cucumber::Feature(f, ev)) => match ev { diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index 86d63bd2..7b1dae9a 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -17,10 +17,11 @@ use std::sync::Arc; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use derive_more::Deref; -use crate::{event, parser, ArbitraryWriter, FailureWriter, World, Writer}; +use crate::{ + event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, +}; /// [`Writer`]-wrapper for transforming [`Skipped`] [`Step`]s into [`Failed`]. /// @@ -64,8 +65,7 @@ where async fn handle_event( &mut self, - ev: parser::Result>, - at: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ) { use event::{ @@ -79,25 +79,33 @@ where Step::Skipped }; - Ok(Cucumber::scenario(f, r, sc, Scenario::Step(st, event))) + Cucumber::scenario(f, r, sc, Scenario::Step(st, event)) }; - let ev = match ev { - Ok(Cucumber::Feature( - f, - Feature::Rule( - r, - Rule::Scenario(sc, Scenario::Step(st, Step::Skipped)), - ), - )) => map_failed(f, Some(r), sc, st), - Ok(Cucumber::Feature( - f, - Feature::Scenario(sc, Scenario::Step(st, Step::Skipped)), - )) => map_failed(f, None, sc, st), - _ => ev, - }; + let ev = ev.map(|ev| { + let (ev, meta) = ev.take(); + + let ev = match ev { + Cucumber::Feature( + f, + Feature::Rule( + r, + Rule::Scenario(sc, Scenario::Step(st, Step::Skipped)), + ), + ) => map_failed(f, Some(r), sc, st), + Cucumber::Feature( + f, + Feature::Scenario(sc, Scenario::Step(st, Step::Skipped)), + ) => map_failed(f, None, sc, st), + Cucumber::Started + | Cucumber::Feature(..) + | Cucumber::Finished => ev, + }; + + meta.insert(ev) + }); - self.writer.handle_event(ev, at, cli).await; + self.writer.handle_event(ev, cli).await; } } diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 3d7c8d1e..1acf9476 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -20,11 +20,10 @@ pub mod summarized; pub mod term; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use sealed::sealed; use structopt::StructOptInternal; -use crate::{event, parser, World}; +use crate::{event, parser, Event, World}; #[doc(inline)] pub use self::{ @@ -63,8 +62,7 @@ pub trait Writer { /// [`Cucumber`]: crate::event::Cucumber async fn handle_event( &mut self, - ev: parser::Result>, - at: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ); } @@ -175,7 +173,7 @@ pub trait Ext: Writer + Sized { #[must_use] fn repeat_if(self, filter: F) -> Repeat where - F: Fn(&parser::Result>) -> bool; + F: Fn(&parser::Result>>) -> bool; } #[sealed] @@ -217,7 +215,7 @@ where fn repeat_if(self, filter: F) -> Repeat where - F: Fn(&parser::Result>) -> bool, + F: Fn(&parser::Result>>) -> bool, { Repeat::new(self, filter) } diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 9ce7c85a..617599d4 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -10,17 +10,15 @@ //! [`Writer`]-wrapper for outputting events in a normalized readable order. -use std::{hash::Hash, sync::Arc}; +use std::{hash::Hash, mem, sync::Arc}; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use derive_more::Deref; use either::Either; use linked_hash_map::LinkedHashMap; use crate::{ - event::{self, DateTimed}, - parser, ArbitraryWriter, FailureWriter, World, Writer, + event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, }; /// Wrapper for a [`Writer`] implementation for outputting events corresponding @@ -57,7 +55,7 @@ impl Normalized { pub fn new(writer: Writer) -> Self { Self { writer, - queue: CucumberQueue::new(Utc::now()), + queue: CucumberQueue::new(Event::new(())), } } } @@ -68,8 +66,7 @@ impl> Writer for Normalized { async fn handle_event( &mut self, - ev: parser::Result>, - at: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ) { use event::{Cucumber, Feature, Rule}; @@ -78,28 +75,36 @@ impl> Writer for Normalized { // without any normalization. // This is done to avoid panic if this `Writer` happens to be wrapped // inside `writer::Repeat` or similar. - if self.queue.is_finished() { - self.writer.handle_event(ev, at, cli).await; + if self.queue.is_finished_and_emitted() { + self.writer.handle_event(ev, cli).await; return; } - match ev { - res @ (Err(_) | Ok(Cucumber::Started)) => { - self.writer.handle_event(res, at, cli).await; + match ev.map(Event::take) { + res @ (Err(_) | Ok((Cucumber::Started, _))) => { + self.writer + .handle_event(res.map(|(ev, meta)| meta.insert(ev)), cli) + .await; } - Ok(Cucumber::Finished) => self.queue.finished(at), - Ok(Cucumber::Feature(f, ev)) => match ev { - Feature::Started => self.queue.new_feature(f, at), + Ok((Cucumber::Finished, meta)) => self.queue.finished(meta), + Ok((Cucumber::Feature(f, ev), meta)) => match ev { + Feature::Started => self.queue.new_feature(f, meta), Feature::Scenario(s, ev) => { - self.queue.insert_scenario_event(&f, None, s, ev, at); + self.queue.insert_scenario_event(&f, None, s, ev, meta); } - Feature::Finished => self.queue.feature_finished(&f, at), + Feature::Finished => self.queue.feature_finished(&f, meta), Feature::Rule(r, ev) => match ev { - Rule::Started => self.queue.new_rule(&f, r, at), - Rule::Scenario(s, e) => { - self.queue.insert_scenario_event(&f, Some(r), s, e, at); + Rule::Started => self.queue.new_rule(&f, r, meta), + Rule::Scenario(s, ev) => { + self.queue.insert_scenario_event( + &f, + Some(r), + s, + ev, + meta, + ); } - Rule::Finished => self.queue.rule_finished(&f, r, at), + Rule::Finished => self.queue.rule_finished(&f, r, meta), }, }, } @@ -110,9 +115,9 @@ impl> Writer for Normalized { self.queue.remove(&feature_to_remove); } - if let Some(at) = self.queue.finished_at { + if let Some(rest) = self.queue.finished_state.take_to_emit() { self.writer - .handle_event(Ok(Cucumber::Finished), at, cli) + .handle_event(Ok(rest.insert(Cucumber::Finished)), cli) .await; } } @@ -165,51 +170,66 @@ struct Queue { /// Underlying FIFO queue of values. queue: LinkedHashMap, - /// [`DateTime`] when this [`Queue`] was created. + /// [`Event`] of [`Queue`] creation. /// - /// This value corresponds to the [`DateTime`] of the `Started` event for - /// item, this [`Queue`] is representing. - started_at: DateTime, + /// If this value is [`Some`], this means `Started` [`Event`] hasn't + /// been passed on to the inner [`Writer`] yet. + started: Option>, + + /// [`FinishedState`] of this [`Queue`]. + finished_state: FinishedState, +} - /// Indicator whether this [`Queue`] started emitting values. - started_emitted: bool, +/// Finishing state of the [`Queue`]. +#[derive(Clone, Debug)] +enum FinishedState { + /// `Finished` event hasn't been encountered yet. + NotFinished, - /// [`DateTime`] when [`Queue`] finished emitting values. + /// `Finished` event encountered, but not passed on to the inner [`Writer`]. /// - /// This value corresponds to the [`DateTime`] of the `Finished` event for - /// item, this [`Queue`] is representing. - finished_at: Option>, + /// This happens when output is busy outputting some other item. + FinishedButNotEmitted(Event<()>), + + /// `Finished` event encountered and passed on to the inner [`Writer`]. + FinishedAndEmitted, +} + +impl FinishedState { + /// If `self` is [`FinishedButNotEmitted`] returns [`Event`], replacing + /// `self` with [`FinishedAndEmitted`]. + /// + /// [`FinishedAndEmitted`]: FinishedState::FinishedAndEmitted + /// [`FinishedButNotEmitted`]: FinishedState::FinishedButNotEmitted + fn take_to_emit(&mut self) -> Option> { + let current = mem::replace(self, Self::FinishedAndEmitted); + if let Self::FinishedButNotEmitted(ev) = current { + Some(ev) + } else { + *self = current; + None + } + } } impl Queue { /// Creates a new empty normalization [`Queue`]. - fn new(started: DateTime) -> Self { + fn new(started: Event<()>) -> Self { Self { queue: LinkedHashMap::new(), - started_at: started, - started_emitted: false, - finished_at: None, + started: Some(started), + finished_state: FinishedState::NotFinished, } } - /// Marks that [`Queue`]'s started event has been emitted. - fn started_emitted(&mut self) { - self.started_emitted = true; - } - - /// Checks whether [`Queue`]'s started event has been emitted. - fn is_started_emitted(&self) -> bool { - self.started_emitted - } - /// Marks this [`Queue`] as finished. - fn finished(&mut self, at: DateTime) { - self.finished_at = Some(at); + fn finished(&mut self, meta: Event<()>) { + self.finished_state = FinishedState::FinishedButNotEmitted(meta); } /// Checks whether this [`Queue`] has been finished. - fn is_finished(&self) -> bool { - self.finished_at.is_some() + fn is_finished_and_emitted(&self) -> bool { + matches!(self.finished_state, FinishedState::FinishedAndEmitted) } /// Removes the given `key` from this [`Queue`]. @@ -283,8 +303,8 @@ impl CucumberQueue { /// Inserts a new [`Feature`] on [`event::Feature::Started`]. /// /// [`Feature`]: gherkin::Feature - fn new_feature(&mut self, feat: Arc, at: DateTime) { - drop(self.queue.insert(feat, FeatureQueue::new(at))); + fn new_feature(&mut self, feat: Arc, meta: Event<()>) { + drop(self.queue.insert(feat, FeatureQueue::new(meta))); } /// Marks a [`Feature`] as finished on [`event::Feature::Finished`]. @@ -293,11 +313,11 @@ impl CucumberQueue { /// [`Feature`]s holding the output. /// /// [`Feature`]: gherkin::Feature - fn feature_finished(&mut self, feat: &gherkin::Feature, at: DateTime) { + fn feature_finished(&mut self, feat: &gherkin::Feature, meta: Event<()>) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .finished(at); + .finished(meta); } /// Inserts a new [`Rule`] on [`event::Rule::Started`]. @@ -307,12 +327,12 @@ impl CucumberQueue { &mut self, feat: &gherkin::Feature, rule: Arc, - at: DateTime, + meta: Event<()>, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .new_rule(rule, at); + .new_rule(rule, meta); } /// Marks a [`Rule`] as finished on [`event::Rule::Finished`]. @@ -325,12 +345,12 @@ impl CucumberQueue { &mut self, feat: &gherkin::Feature, rule: Arc, - at: DateTime, + meta: Event<()>, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .rule_finished(rule, at); + .rule_finished(rule, meta); } /// Inserts a new [`event::Scenario::Started`]. @@ -340,12 +360,12 @@ impl CucumberQueue { rule: Option>, scenario: Arc, event: event::Scenario, - at: DateTime, + meta: Event<()>, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .insert_scenario_event(rule, scenario, event, at); + .insert_scenario_event(rule, scenario, event, meta); } } @@ -369,15 +389,15 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { cli: &W::Cli, ) -> Option { if let Some((f, events)) = self.current_item() { - if !events.is_started_emitted() { + if let Some(meta) = events.started.take() { writer .handle_event( - Ok(event::Cucumber::feature_started(Arc::clone(&f))), - events.started_at, + Ok(meta.insert(event::Cucumber::feature_started( + Arc::clone(&f), + ))), cli, ) .await; - events.started_emitted(); } while let Some(scenario_or_rule_to_remove) = @@ -386,11 +406,12 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { events.remove(&scenario_or_rule_to_remove); } - if let Some(at) = events.finished_at { + if let Some(meta) = events.finished_state.take_to_emit() { writer .handle_event( - Ok(event::Cucumber::feature_finished(Arc::clone(&f))), - at, + Ok(meta.insert(event::Cucumber::feature_finished( + Arc::clone(&f), + ))), cli, ) .await; @@ -433,20 +454,22 @@ impl FeatureQueue { /// Inserts a new [`Rule`]. /// /// [`Rule`]: gherkin::Rule - fn new_rule(&mut self, rule: Arc, at: DateTime) { + fn new_rule(&mut self, rule: Arc, meta: Event<()>) { drop( - self.queue - .insert(Either::Left(rule), Either::Left(RulesQueue::new(at))), + self.queue.insert( + Either::Left(rule), + Either::Left(RulesQueue::new(meta)), + ), ); } /// Marks a [`Rule`] as finished on [`event::Rule::Finished`]. /// /// [`Rule`]: gherkin::Rule - fn rule_finished(&mut self, rule: Arc, at: DateTime) { + fn rule_finished(&mut self, rule: Arc, meta: Event<()>) { match self.queue.get_mut(&Either::Left(rule)).unwrap() { Either::Left(ev) => { - ev.finished(at); + ev.finished(meta); } Either::Right(_) => unreachable!(), } @@ -460,7 +483,7 @@ impl FeatureQueue { rule: Option>, scenario: Arc, ev: event::Scenario, - at: DateTime, + meta: Event<()>, ) { if let Some(rule) = rule { match self @@ -473,7 +496,7 @@ impl FeatureQueue { .entry(scenario) .or_insert_with(ScenariosQueue::new) .0 - .push(DateTimed::at(ev, at)), + .push((ev, meta)), Either::Right(_) => unreachable!(), } } else { @@ -482,7 +505,7 @@ impl FeatureQueue { .entry(Either::Right(scenario)) .or_insert_with(|| Either::Right(ScenariosQueue::new())) { - Either::Right(events) => events.0.push(DateTimed::at(ev, at)), + Either::Right(events) => events.0.push((ev, meta)), Either::Left(_) => unreachable!(), } } @@ -550,18 +573,16 @@ impl<'me, World> Emitter for &'me mut RulesQueue { writer: &mut W, cli: &W::Cli, ) -> Option { - if !self.is_started_emitted() { + if let Some(meta) = self.started.take() { writer .handle_event( - Ok(event::Cucumber::rule_started( + Ok(meta.insert(event::Cucumber::rule_started( Arc::clone(&feature), Arc::clone(&rule), - )), - self.started_at, + ))), cli, ) .await; - self.started_emitted(); } while let Some((scenario, events)) = self.current_item() { @@ -579,14 +600,13 @@ impl<'me, World> Emitter for &'me mut RulesQueue { } } - if let Some(at) = self.finished_at { + if let Some(meta) = self.finished_state.take_to_emit() { writer .handle_event( - Ok(event::Cucumber::rule_finished( + Ok(meta.insert(event::Cucumber::rule_finished( feature, Arc::clone(&rule), - )), - at, + ))), cli, ) .await; @@ -601,7 +621,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { /// /// [`Scenario`]: gherkin::Scenario #[derive(Debug)] -struct ScenariosQueue(Vec>>); +struct ScenariosQueue(Vec<(event::Scenario, Event<()>)>); impl ScenariosQueue { /// Creates a new [`ScenariosQueue`]. @@ -612,7 +632,7 @@ impl ScenariosQueue { #[async_trait(?Send)] impl Emitter for &mut ScenariosQueue { - type Current = DateTimed>; + type Current = (event::Scenario, Event<()>); type Emitted = Arc; type EmittedPath = ( Arc, @@ -630,16 +650,17 @@ impl Emitter for &mut ScenariosQueue { writer: &mut W, cli: &W::Cli, ) -> Option { - while let Some(DateTimed { inner: ev, at }) = self.current_item() { + while let Some((ev, meta)) = self.current_item() { let should_be_removed = matches!(ev, event::Scenario::Finished); - let ev = event::Cucumber::scenario( + let ev = meta.insert(event::Cucumber::scenario( Arc::clone(&feature), rule.as_ref().map(Arc::clone), Arc::clone(&scenario), ev, - ); - writer.handle_event(Ok(ev), at, cli).await; + )); + + writer.handle_event(Ok(ev), cli).await; if should_be_removed { return Some(Arc::clone(&scenario)); diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 3bd92f4b..774a85ce 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -13,12 +13,10 @@ use std::mem; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use derive_more::Deref; use crate::{ - event::{self, DateTimed}, - parser, ArbitraryWriter, FailureWriter, World, Writer, + event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, }; /// Wrapper for a [`Writer`] implementation for re-outputting events at the end @@ -39,39 +37,42 @@ pub struct Repeat> { filter: F, /// Buffer of collected events for re-outputting. - events: Vec>>>, + events: Vec>>>, } /// Alias for a [`fn`] predicate deciding whether an event should be /// re-outputted or not. -pub type FilterEvent = fn(&parser::Result>) -> bool; +pub type FilterEvent = + fn(&parser::Result>>) -> bool; #[async_trait(?Send)] impl Writer for Repeat where W: World, Wr: Writer, - F: Fn(&parser::Result>) -> bool, + F: Fn(&parser::Result>>) -> bool, { type Cli = Wr::Cli; async fn handle_event( &mut self, - ev: parser::Result>, - at: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ) { if (self.filter)(&ev) { - self.events.push(DateTimed::at(ev.clone(), at)); + self.events.push(ev.clone()); } - let is_finished = matches!(ev, Ok(event::Cucumber::Finished)); + let is_finished = matches!( + ev.as_ref().map(AsRef::as_ref), + Ok(event::Cucumber::Finished) + ); - self.writer.handle_event(ev, at, cli).await; + self.writer.handle_event(ev, cli).await; if is_finished { - for DateTimed { inner: ev, at } in mem::take(&mut self.events) { - self.writer.handle_event(ev, at, cli).await; + for ev in mem::take(&mut self.events) { + self.writer.handle_event(ev, cli).await; } } } @@ -83,7 +84,7 @@ where W: World, Wr: ArbitraryWriter<'val, W, Val>, Val: 'val, - F: Fn(&parser::Result>) -> bool, + F: Fn(&parser::Result>>) -> bool, { async fn write(&mut self, val: Val) where @@ -137,7 +138,7 @@ impl Repeat { writer, filter: |ev| { matches!( - ev, + ev.as_ref().map(AsRef::as_ref), Ok(Cucumber::Feature( _, Feature::Rule( @@ -172,7 +173,7 @@ impl Repeat { writer, filter: |ev| { matches!( - ev, + ev.as_ref().map(AsRef::as_ref), Ok(Cucumber::Feature( _, Feature::Rule( diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index 707fbdc2..241e082b 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -13,13 +13,12 @@ use std::{array, borrow::Cow, collections::HashMap, sync::Arc}; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use derive_more::Deref; use itertools::Itertools as _; use crate::{ - event, parser, writer::term::Styles, ArbitraryWriter, FailureWriter, World, - Writer, + event, parser, writer::term::Styles, ArbitraryWriter, Event, FailureWriter, + World, Writer, }; /// Execution statistics. @@ -146,14 +145,14 @@ where async fn handle_event( &mut self, - ev: parser::Result>, - at: DateTime, + ev: parser::Result>>, cli: &Self::Cli, ) { use event::{Cucumber, Feature, Rule}; let mut finished = false; - match &ev { + + match ev.as_ref().map(AsRef::as_ref) { Err(_) => self.parsing_errors += 1, Ok(Cucumber::Feature(_, ev)) => match ev { Feature::Started => self.features += 1, @@ -170,7 +169,7 @@ where Ok(Cucumber::Started) => {} }; - self.writer.handle_event(ev, at, cli).await; + self.writer.handle_event(ev, cli).await; if finished { self.writer.write(Styles::new().summary(self)).await; diff --git a/tests/features/output/ambiguous_step.feature.out b/tests/features/output/ambiguous_step.feature.out index 38667cb2..8e9510b5 100644 --- a/tests/features/output/ambiguous_step.feature.out +++ b/tests/features/output/ambiguous_step.feature.out @@ -2,7 +2,7 @@ Started Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Started) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Started)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Started))) -Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 15, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 23, column: 1 }))] }))))) +Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Step(Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }, Failed(None, None, AmbiguousMatch(AmbiguousMatchError { possible_matches: [(HashableRegex(foo is (\d+)), Some(Location { line: 14, column: 1 })), (HashableRegex(foo is (\d+) ambiguous), Some(Location { line: 22, column: 1 }))] }))))) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Scenario(Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }, Finished)) Feature(Feature { keyword: "Feature", name: "ambiguous", description: None, background: None, scenarios: [Scenario { keyword: "Scenario", name: "ambiguous", steps: [Step { keyword: "Given", ty: Given, value: "foo is 0 ambiguous", docstring: None, table: None, position: LineCol { line: 3, col: 5 } }], examples: None, tags: [], position: LineCol { line: 2, col: 3 } }], rules: [], tags: [], position: LineCol { line: 1, col: 1 }, }, Finished) Finished diff --git a/tests/output.rs b/tests/output.rs index b6330762..e9f6cc54 100644 --- a/tests/output.rs +++ b/tests/output.rs @@ -1,9 +1,8 @@ use std::{borrow::Cow, cmp::Ordering, convert::Infallible, fmt::Debug}; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use cucumber::{ - cli, event, given, parser, step, then, when, WorldInit, Writer, + cli, event, given, parser, step, then, when, Event, WorldInit, Writer, }; use itertools::Itertools as _; use once_cell::sync::Lazy; @@ -43,8 +42,7 @@ impl Writer for DebugWriter { #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn handle_event( &mut self, - ev: parser::Result>, - _: DateTime, + ev: parser::Result>>, _: &Self::Cli, ) { use event::{Cucumber, Feature, Rule, Scenario, Step, StepError}; @@ -69,7 +67,7 @@ impl Writer for DebugWriter { e }; - let ev: Cow<_> = match ev { + let ev: Cow<_> = match ev.map(Event::into_inner) { Err(_) => "ParsingError".into(), Ok(Cucumber::Feature( feat, From 7808d76a51c8269157f6fb0e5a29a3ee9d67ce5a Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 26 Oct 2021 16:53:49 +0300 Subject: [PATCH 25/27] Correction --- src/event.rs | 1 + src/writer/normalized.rs | 5 ++--- src/writer/summarized.rs | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/event.rs b/src/event.rs index 64e0f022..293c4000 100644 --- a/src/event.rs +++ b/src/event.rs @@ -38,6 +38,7 @@ pub type Info = Arc; /// Metadata is configured with cargo features. This is done mainly to give us /// ability to add additional fields without introducing breaking changes. #[derive(AsRef, Clone, Debug)] +#[non_exhaustive] pub struct Event { /// [`SystemTime`] when [`Event`] happened. #[cfg(feature = "event-time")] diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 617599d4..873dde87 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -115,9 +115,9 @@ impl> Writer for Normalized { self.queue.remove(&feature_to_remove); } - if let Some(rest) = self.queue.finished_state.take_to_emit() { + if let Some(meta) = self.queue.finished_state.take_to_emit() { self.writer - .handle_event(Ok(rest.insert(Cucumber::Finished)), cli) + .handle_event(Ok(meta.insert(Cucumber::Finished)), cli) .await; } } @@ -659,7 +659,6 @@ impl Emitter for &mut ScenariosQueue { Arc::clone(&scenario), ev, )); - writer.handle_event(Ok(ev), cli).await; if should_be_removed { diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index 241e082b..2a5ab079 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -151,7 +151,6 @@ where use event::{Cucumber, Feature, Rule}; let mut finished = false; - match ev.as_ref().map(AsRef::as_ref) { Err(_) => self.parsing_errors += 1, Ok(Cucumber::Feature(_, ev)) => match ev { From 39ba0df8287acb44e38699f487284002a4b80f4a Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 26 Oct 2021 19:04:24 +0300 Subject: [PATCH 26/27] Corrections [run ci] --- Cargo.toml | 4 +- Makefile | 2 +- src/cucumber.rs | 2 +- src/event.rs | 87 +++++++++------- src/runner/basic.rs | 4 +- src/writer/fail_on_skipped.rs | 8 +- src/writer/normalized.rs | 180 ++++++++++++++++++---------------- src/writer/repeat.rs | 14 ++- src/writer/summarized.rs | 2 +- tests/output.rs | 2 - 10 files changed, 165 insertions(+), 140 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 41f8d154..2880b869 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,10 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["macros"] +# Enables step attributes and auto-wiring. macros = ["cucumber-codegen", "inventory"] -event-time = [] +# Enables timestamps collecting for all events. +timestamps = [] [dependencies] async-trait = "0.1.40" diff --git a/Makefile b/Makefile index dae5a41d..184fb48c 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ cargo.fmt: # make cargo.lint cargo.lint: - cargo clippy --workspace -- -D warnings + cargo clippy --workspace --all-features -- -D warnings diff --git a/src/cucumber.rs b/src/cucumber.rs index 9b0d3742..8599605a 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -388,7 +388,7 @@ where /// use cucumber::event::{Cucumber, Feature, Rule, Scenario, Step}; /// /// matches!( - /// ev.as_ref().map(AsRef::as_ref), + /// ev.as_deref(), /// Ok(Cucumber::Feature( /// _, /// Feature::Rule( diff --git a/src/event.rs b/src/event.rs index 293c4000..ea2ffa87 100644 --- a/src/event.rs +++ b/src/event.rs @@ -21,10 +21,10 @@ use std::{any::Any, fmt, sync::Arc}; -#[cfg(feature = "event-time")] +#[cfg(feature = "timestamps")] use std::time::SystemTime; -use derive_more::{AsRef, Display, Error, From}; +use derive_more::{AsRef, Deref, DerefMut, Display, Error, From}; use crate::{step, writer::basic::coerce_error}; @@ -33,66 +33,85 @@ use crate::{step, writer::basic::coerce_error}; /// [`catch_unwind()`]: std::panic::catch_unwind() pub type Info = Arc; -/// [`Cucumber`] event paired with additional metadata. +/// Arbitrary event, optionally paired with additional metadata. /// -/// Metadata is configured with cargo features. This is done mainly to give us -/// ability to add additional fields without introducing breaking changes. -#[derive(AsRef, Clone, Debug)] +/// Any metadata is added by enabling the correspondent library feature: +/// - `timestamps`: adds time of when this [`Event`] has happened. +#[derive(AsRef, Clone, Copy, Debug, Deref, DerefMut)] #[non_exhaustive] pub struct Event { - /// [`SystemTime`] when [`Event`] happened. - #[cfg(feature = "event-time")] + /// [`SystemTime`] when this [`Event`] has happened. + #[cfg(feature = "timestamps")] pub at: SystemTime, - /// Inner value. + /// Actual value of this [`Event`]. #[as_ref] - pub inner: T, + #[deref] + #[deref_mut] + pub value: T, } impl Event { - /// Creates a new [`Event`]. - // False positive: SystemTime::now() isn't const - #[allow(clippy::missing_const_for_fn)] + /// Creates a new [`Event`] out of the given `value`. #[must_use] - pub fn new(inner: T) -> Self { + pub fn new(value: T) -> Self { Self { - #[cfg(feature = "event-time")] + #[cfg(feature = "timestamps")] at: SystemTime::now(), - inner, + value, } } - /// Returns [`Event::inner`]. - // False positive: `constant functions cannot evaluate destructors` - #[allow(clippy::missing_const_for_fn)] + /// Unwraps the inner [`Event::value`] loosing all the attached metadata. + #[allow(clippy::missing_const_for_fn)] // false positive: drop in const #[must_use] pub fn into_inner(self) -> T { - self.inner + self.value } - /// Replaces [`Event::inner`] with `()`, returning the old one. - pub fn take(self) -> (T, Event<()>) { + /// Splits this [`Event`] to the inner [`Event::value`] and its detached + /// metadata. + #[must_use] + pub fn split(self) -> (T, Metadata) { self.replace(()) } - /// Replaces [`Event::inner`] with `value`, dropping the old one. + /// Replaces the inner [`Event::value`] with the given one, dropping the old + /// one in place. #[must_use] pub fn insert(self, value: V) -> Event { self.replace(value).1 } - /// Replaces [`Event::inner`] with `value`, returning the old one. - // False positive: `constant functions cannot evaluate destructors` - #[allow(clippy::missing_const_for_fn)] + /// Maps the inner [`Event::value`] with the given function. + #[must_use] + pub fn map(self, f: impl FnOnce(T) -> V) -> Event { + let (val, meta) = self.split(); + meta.insert(f(val)) + } + + /// Replaces the inner [`Event::value`] with the given one, returning the + /// old one along. + #[allow(clippy::missing_const_for_fn)] // false positive: drop in const + #[must_use] pub fn replace(self, value: V) -> (T, Event) { - ( - self.inner, - Event { - inner: value, - #[cfg(feature = "event-time")] - at: self.at, - }, - ) + let event = Event { + #[cfg(feature = "timestamps")] + at: self.at, + value, + }; + (self.value, event) + } +} + +/// Shortcut for a detached metadata of an arbitrary [`Event`]. +pub type Metadata = Event<()>; + +impl Metadata { + /// Wraps the given `value` with this [`Event`] metadata. + #[must_use] + pub fn wrap(self, value: V) -> Event { + self.replace(value).1 } } diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 38097abf..4fdefee2 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -1107,10 +1107,10 @@ where /// /// [`Cucumber`]: event::Cucumber fn send_all(&self, events: impl Iterator>) { - for ev in events { + 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.sender.unbounded_send(Ok(Event::new(ev))).is_err() { + if self.sender.unbounded_send(Ok(Event::new(v))).is_err() { break; } } diff --git a/src/writer/fail_on_skipped.rs b/src/writer/fail_on_skipped.rs index 7b1dae9a..02fa970f 100644 --- a/src/writer/fail_on_skipped.rs +++ b/src/writer/fail_on_skipped.rs @@ -83,9 +83,7 @@ where }; let ev = ev.map(|ev| { - let (ev, meta) = ev.take(); - - let ev = match ev { + ev.map(|ev| match ev { Cucumber::Feature( f, Feature::Rule( @@ -100,9 +98,7 @@ where Cucumber::Started | Cucumber::Feature(..) | Cucumber::Finished => ev, - }; - - meta.insert(ev) + }) }); self.writer.handle_event(ev, cli).await; diff --git a/src/writer/normalized.rs b/src/writer/normalized.rs index 873dde87..c377ce3f 100644 --- a/src/writer/normalized.rs +++ b/src/writer/normalized.rs @@ -18,7 +18,8 @@ use either::Either; use linked_hash_map::LinkedHashMap; use crate::{ - event, parser, ArbitraryWriter, Event, FailureWriter, World, Writer, + event::{self, Metadata}, + parser, ArbitraryWriter, Event, FailureWriter, World, Writer, }; /// Wrapper for a [`Writer`] implementation for outputting events corresponding @@ -55,7 +56,7 @@ impl Normalized { pub fn new(writer: Writer) -> Self { Self { writer, - queue: CucumberQueue::new(Event::new(())), + queue: CucumberQueue::new(Metadata::new(())), } } } @@ -80,7 +81,7 @@ impl> Writer for Normalized { return; } - match ev.map(Event::take) { + match ev.map(Event::split) { res @ (Err(_) | Ok((Cucumber::Started, _))) => { self.writer .handle_event(res.map(|(ev, meta)| meta.insert(ev)), cli) @@ -88,23 +89,29 @@ impl> Writer for Normalized { } Ok((Cucumber::Finished, meta)) => self.queue.finished(meta), Ok((Cucumber::Feature(f, ev), meta)) => match ev { - Feature::Started => self.queue.new_feature(f, meta), + Feature::Started => self.queue.new_feature(meta.wrap(f)), Feature::Scenario(s, ev) => { - self.queue.insert_scenario_event(&f, None, s, ev, meta); + self.queue.insert_scenario_event( + &f, + None, + s, + meta.wrap(ev), + ); } - Feature::Finished => self.queue.feature_finished(&f, meta), + Feature::Finished => self.queue.feature_finished(meta.wrap(&f)), Feature::Rule(r, ev) => match ev { - Rule::Started => self.queue.new_rule(&f, r, meta), + Rule::Started => self.queue.new_rule(&f, meta.wrap(r)), Rule::Scenario(s, ev) => { self.queue.insert_scenario_event( &f, Some(r), s, - ev, - meta, + meta.wrap(ev), ); } - Rule::Finished => self.queue.rule_finished(&f, r, meta), + Rule::Finished => { + self.queue.rule_finished(&f, meta.wrap(r)); + } }, }, } @@ -115,9 +122,9 @@ impl> Writer for Normalized { self.queue.remove(&feature_to_remove); } - if let Some(meta) = self.queue.finished_state.take_to_emit() { + if let Some(meta) = self.queue.state.take_to_emit() { self.writer - .handle_event(Ok(meta.insert(Cucumber::Finished)), cli) + .handle_event(Ok(meta.wrap(Cucumber::Finished)), cli) .await; } } @@ -170,41 +177,72 @@ struct Queue { /// Underlying FIFO queue of values. queue: LinkedHashMap, - /// [`Event`] of [`Queue`] creation. + /// Initial [`Metadata`] of this [`Queue`] creation. /// - /// If this value is [`Some`], this means `Started` [`Event`] hasn't - /// been passed on to the inner [`Writer`] yet. - started: Option>, + /// If this value is [`Some`], then `Started` [`Event`] hasn't been passed + /// on to the inner [`Writer`] yet. + initial: Option, /// [`FinishedState`] of this [`Queue`]. - finished_state: FinishedState, + state: FinishedState, +} + +impl Queue { + /// Creates a new normalization [`Queue`] with an initial metadata. + fn new(initial: Metadata) -> Self { + Self { + queue: LinkedHashMap::new(), + initial: Some(initial), + state: FinishedState::NotFinished, + } + } + + /// Marks this [`Queue`] as [`FinishedButNotEmitted`]. + /// + /// [`FinishedButNotEmitted`]: FinishedState::FinishedButNotEmitted + fn finished(&mut self, meta: Metadata) { + self.state = FinishedState::FinishedButNotEmitted(meta); + } + + /// Checks whether this [`Queue`] transited to [`FinishedAndEmitted`] state. + /// + /// [`FinishedAndEmitted`]: FinishedState::FinishedAndEmitted + fn is_finished_and_emitted(&self) -> bool { + matches!(self.state, FinishedState::FinishedAndEmitted) + } + + /// Removes the given `key` from this [`Queue`]. + fn remove(&mut self, key: &K) { + drop(self.queue.remove(key)); + } } -/// Finishing state of the [`Queue`]. -#[derive(Clone, Debug)] +/// Finishing state of a [`Queue`]. +#[derive(Clone, Copy, Debug)] enum FinishedState { /// `Finished` event hasn't been encountered yet. NotFinished, - /// `Finished` event encountered, but not passed on to the inner [`Writer`]. + /// `Finished` event has been encountered, but not passed to the inner + /// [`Writer`] yet. /// - /// This happens when output is busy outputting some other item. - FinishedButNotEmitted(Event<()>), + /// This happens when output is busy due to outputting some other item. + FinishedButNotEmitted(Metadata), - /// `Finished` event encountered and passed on to the inner [`Writer`]. + /// `Finished` event has been encountered and passed to the inner + /// [`Writer`]. FinishedAndEmitted, } impl FinishedState { - /// If `self` is [`FinishedButNotEmitted`] returns [`Event`], replacing - /// `self` with [`FinishedAndEmitted`]. + /// Returns [`Metadata`] of this [`FinishedState::FinishedButNotEmitted`], + /// and makes it [`FinishedAndEmitted`]. /// /// [`FinishedAndEmitted`]: FinishedState::FinishedAndEmitted - /// [`FinishedButNotEmitted`]: FinishedState::FinishedButNotEmitted - fn take_to_emit(&mut self) -> Option> { + fn take_to_emit(&mut self) -> Option { let current = mem::replace(self, Self::FinishedAndEmitted); - if let Self::FinishedButNotEmitted(ev) = current { - Some(ev) + if let Self::FinishedButNotEmitted(meta) = current { + Some(meta) } else { *self = current; None @@ -212,32 +250,6 @@ impl FinishedState { } } -impl Queue { - /// Creates a new empty normalization [`Queue`]. - fn new(started: Event<()>) -> Self { - Self { - queue: LinkedHashMap::new(), - started: Some(started), - finished_state: FinishedState::NotFinished, - } - } - - /// Marks this [`Queue`] as finished. - fn finished(&mut self, meta: Event<()>) { - self.finished_state = FinishedState::FinishedButNotEmitted(meta); - } - - /// Checks whether this [`Queue`] has been finished. - fn is_finished_and_emitted(&self) -> bool { - matches!(self.finished_state, FinishedState::FinishedAndEmitted) - } - - /// Removes the given `key` from this [`Queue`]. - fn remove(&mut self, key: &K) { - drop(self.queue.remove(key)); - } -} - /// [`Queue`] which can remember its current item ([`Feature`], [`Rule`], /// [`Scenario`] or [`Step`]) and pass events connected to it to the provided /// [`Writer`]. @@ -303,7 +315,8 @@ impl CucumberQueue { /// Inserts a new [`Feature`] on [`event::Feature::Started`]. /// /// [`Feature`]: gherkin::Feature - fn new_feature(&mut self, feat: Arc, meta: Event<()>) { + fn new_feature(&mut self, feat: Event>) { + let (feat, meta) = feat.split(); drop(self.queue.insert(feat, FeatureQueue::new(meta))); } @@ -313,7 +326,8 @@ impl CucumberQueue { /// [`Feature`]s holding the output. /// /// [`Feature`]: gherkin::Feature - fn feature_finished(&mut self, feat: &gherkin::Feature, meta: Event<()>) { + fn feature_finished(&mut self, feat: Event<&gherkin::Feature>) { + let (feat, meta) = feat.split(); self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) @@ -326,13 +340,12 @@ impl CucumberQueue { fn new_rule( &mut self, feat: &gherkin::Feature, - rule: Arc, - meta: Event<()>, + rule: Event>, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .new_rule(rule, meta); + .new_rule(rule); } /// Marks a [`Rule`] as finished on [`event::Rule::Finished`]. @@ -344,13 +357,12 @@ impl CucumberQueue { fn rule_finished( &mut self, feat: &gherkin::Feature, - rule: Arc, - meta: Event<()>, + rule: Event>, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .rule_finished(rule, meta); + .rule_finished(rule); } /// Inserts a new [`event::Scenario::Started`]. @@ -359,13 +371,12 @@ impl CucumberQueue { feat: &gherkin::Feature, rule: Option>, scenario: Arc, - event: event::Scenario, - meta: Event<()>, + event: Event>, ) { self.queue .get_mut(feat) .unwrap_or_else(|| panic!("No Feature {}", feat.name)) - .insert_scenario_event(rule, scenario, event, meta); + .insert_scenario_event(rule, scenario, event); } } @@ -389,10 +400,10 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { cli: &W::Cli, ) -> Option { if let Some((f, events)) = self.current_item() { - if let Some(meta) = events.started.take() { + if let Some(meta) = events.initial.take() { writer .handle_event( - Ok(meta.insert(event::Cucumber::feature_started( + Ok(meta.wrap(event::Cucumber::feature_started( Arc::clone(&f), ))), cli, @@ -406,10 +417,10 @@ impl<'me, World> Emitter for &'me mut CucumberQueue { events.remove(&scenario_or_rule_to_remove); } - if let Some(meta) = events.finished_state.take_to_emit() { + if let Some(meta) = events.state.take_to_emit() { writer .handle_event( - Ok(meta.insert(event::Cucumber::feature_finished( + Ok(meta.wrap(event::Cucumber::feature_finished( Arc::clone(&f), ))), cli, @@ -454,7 +465,8 @@ impl FeatureQueue { /// Inserts a new [`Rule`]. /// /// [`Rule`]: gherkin::Rule - fn new_rule(&mut self, rule: Arc, meta: Event<()>) { + fn new_rule(&mut self, rule: Event>) { + let (rule, meta) = rule.split(); drop( self.queue.insert( Either::Left(rule), @@ -466,7 +478,8 @@ impl FeatureQueue { /// Marks a [`Rule`] as finished on [`event::Rule::Finished`]. /// /// [`Rule`]: gherkin::Rule - fn rule_finished(&mut self, rule: Arc, meta: Event<()>) { + fn rule_finished(&mut self, rule: Event>) { + let (rule, meta) = rule.split(); match self.queue.get_mut(&Either::Left(rule)).unwrap() { Either::Left(ev) => { ev.finished(meta); @@ -482,8 +495,7 @@ impl FeatureQueue { &mut self, rule: Option>, scenario: Arc, - ev: event::Scenario, - meta: Event<()>, + ev: Event>, ) { if let Some(rule) = rule { match self @@ -496,7 +508,7 @@ impl FeatureQueue { .entry(scenario) .or_insert_with(ScenariosQueue::new) .0 - .push((ev, meta)), + .push(ev), Either::Right(_) => unreachable!(), } } else { @@ -505,7 +517,7 @@ impl FeatureQueue { .entry(Either::Right(scenario)) .or_insert_with(|| Either::Right(ScenariosQueue::new())) { - Either::Right(events) => events.0.push((ev, meta)), + Either::Right(events) => events.0.push(ev), Either::Left(_) => unreachable!(), } } @@ -573,10 +585,10 @@ impl<'me, World> Emitter for &'me mut RulesQueue { writer: &mut W, cli: &W::Cli, ) -> Option { - if let Some(meta) = self.started.take() { + if let Some(meta) = self.initial.take() { writer .handle_event( - Ok(meta.insert(event::Cucumber::rule_started( + Ok(meta.wrap(event::Cucumber::rule_started( Arc::clone(&feature), Arc::clone(&rule), ))), @@ -600,10 +612,10 @@ impl<'me, World> Emitter for &'me mut RulesQueue { } } - if let Some(meta) = self.finished_state.take_to_emit() { + if let Some(meta) = self.state.take_to_emit() { writer .handle_event( - Ok(meta.insert(event::Cucumber::rule_finished( + Ok(meta.wrap(event::Cucumber::rule_finished( feature, Arc::clone(&rule), ))), @@ -621,7 +633,7 @@ impl<'me, World> Emitter for &'me mut RulesQueue { /// /// [`Scenario`]: gherkin::Scenario #[derive(Debug)] -struct ScenariosQueue(Vec<(event::Scenario, Event<()>)>); +struct ScenariosQueue(Vec>>); impl ScenariosQueue { /// Creates a new [`ScenariosQueue`]. @@ -632,7 +644,7 @@ impl ScenariosQueue { #[async_trait(?Send)] impl Emitter for &mut ScenariosQueue { - type Current = (event::Scenario, Event<()>); + type Current = Event>; type Emitted = Arc; type EmittedPath = ( Arc, @@ -650,10 +662,10 @@ impl Emitter for &mut ScenariosQueue { writer: &mut W, cli: &W::Cli, ) -> Option { - while let Some((ev, meta)) = self.current_item() { + while let Some((ev, meta)) = self.current_item().map(Event::split) { let should_be_removed = matches!(ev, event::Scenario::Finished); - let ev = meta.insert(event::Cucumber::scenario( + let ev = meta.wrap(event::Cucumber::scenario( Arc::clone(&feature), rule.as_ref().map(Arc::clone), Arc::clone(&scenario), diff --git a/src/writer/repeat.rs b/src/writer/repeat.rs index 774a85ce..6ef01ff2 100644 --- a/src/writer/repeat.rs +++ b/src/writer/repeat.rs @@ -63,10 +63,8 @@ where self.events.push(ev.clone()); } - let is_finished = matches!( - ev.as_ref().map(AsRef::as_ref), - Ok(event::Cucumber::Finished) - ); + let is_finished = + matches!(ev.as_deref(), Ok(event::Cucumber::Finished)); self.writer.handle_event(ev, cli).await; @@ -138,7 +136,7 @@ impl Repeat { writer, filter: |ev| { matches!( - ev.as_ref().map(AsRef::as_ref), + ev.as_deref(), Ok(Cucumber::Feature( _, Feature::Rule( @@ -153,7 +151,7 @@ impl Repeat { Scenario::Step(_, Step::Skipped) | Scenario::Background(_, Step::Skipped) ) - )) + )), ) }, events: Vec::new(), @@ -173,7 +171,7 @@ impl Repeat { writer, filter: |ev| { matches!( - ev.as_ref().map(AsRef::as_ref), + ev.as_deref(), Ok(Cucumber::Feature( _, Feature::Rule( @@ -190,7 +188,7 @@ impl Repeat { | Scenario::Background(_, Step::Failed(..)) | Scenario::Hook(_, Hook::Failed(..)) ) - )) | Err(_) + )) | Err(_), ) }, events: Vec::new(), diff --git a/src/writer/summarized.rs b/src/writer/summarized.rs index 2a5ab079..9200bf0c 100644 --- a/src/writer/summarized.rs +++ b/src/writer/summarized.rs @@ -151,7 +151,7 @@ where use event::{Cucumber, Feature, Rule}; let mut finished = false; - match ev.as_ref().map(AsRef::as_ref) { + match ev.as_deref() { Err(_) => self.parsing_errors += 1, Ok(Cucumber::Feature(_, ev)) => match ev { Feature::Started => self.features += 1, diff --git a/tests/output.rs b/tests/output.rs index e9f6cc54..8b553302 100644 --- a/tests/output.rs +++ b/tests/output.rs @@ -26,7 +26,6 @@ fn ambiguous(_w: &mut World) {} impl cucumber::World for World { type Error = Infallible; - #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn new() -> Result { Ok(World::default()) } @@ -39,7 +38,6 @@ struct DebugWriter(String); impl Writer for DebugWriter { type Cli = cli::Empty; - #[allow(clippy::unused_async)] // false positive: #[async_trait] async fn handle_event( &mut self, ev: parser::Result>>, From 3c4e144ba16ecd439e52edefab13fa8d9b1acb75 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 26 Oct 2021 19:13:12 +0300 Subject: [PATCH 27/27] Add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4093aae..c560839a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - Ability to run `Scenario`s concurrently. ([#128]) - Highlighting of regex capture groups in terminal output with __bold__ style. ([#136]) - Error on a step matching multiple step functions ([#143]). +- `timestamps` Cargo feature that enables collecting of timestamps for all the happened events during tests execution (useful for `Writer`s which format requires them) ([#145]). [#128]: /../../pull/128 [#136]: /../../pull/136 @@ -36,6 +37,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [#142]: /../../pull/142 [#143]: /../../pull/143 [#144]: /../../pull/144 +[#145]: /../../pull/145