From f031fc29acec162989317f07e26690993c6dd953 Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 20 Oct 2021 10:35:54 +0300 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 e0b85c06df69ce7972576250ff62cc8458f26084 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 25 Oct 2021 17:23:38 +0300 Subject: [PATCH 20/21] Restore broken imports --- codegen/src/lib.rs | 4 ++-- src/codegen.rs | 5 +++-- src/lib.rs | 32 ++++++++++++++++---------------- src/parser/mod.rs | 4 ++-- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index ae2975ac..9833d186 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -93,11 +93,11 @@ variant_size_differences )] -use proc_macro::TokenStream; - mod attribute; mod derive; +use proc_macro::TokenStream; + /// Helper macro for generating public shims for [`macro@given`], [`macro@when`] /// and [`macro@then`] attributes. macro_rules! step_attribute { diff --git a/src/codegen.rs b/src/codegen.rs index 505a69f0..3aef7d0c 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -13,12 +13,13 @@ use std::{fmt::Debug, path::Path}; use async_trait::async_trait; + +use crate::{cucumber::DefaultCucumber, step, Cucumber, Step, World}; + pub use futures::future::LocalBoxFuture; pub use inventory::{self, collect, submit}; pub use regex::Regex; -use crate::{cucumber::DefaultCucumber, step, Cucumber, Step, World}; - /// [`World`] extension with auto-wiring capabilities. #[async_trait(?Send)] pub trait WorldInit: WorldInventory diff --git a/src/lib.rs b/src/lib.rs index b586f2e6..0e1dad95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,18 +97,31 @@ )] #![cfg_attr(docsrs, feature(doc_cfg))] +pub mod cli; +mod cucumber; +pub mod event; +pub mod feature; +pub mod parser; +pub mod runner; +pub mod step; +pub mod writer; + +#[cfg(feature = "macros")] +pub mod codegen; + use std::error::Error as StdError; use async_trait::async_trait; + pub use gherkin; #[cfg(feature = "macros")] #[doc(inline)] -pub use cucumber_codegen::{given, then, when, WorldInit}; - +pub use self::codegen::WorldInit; #[cfg(feature = "macros")] #[doc(inline)] -pub use self::codegen::WorldInit; +pub use cucumber_codegen::{given, then, when, WorldInit}; + #[doc(inline)] pub use self::{ cucumber::Cucumber, @@ -121,19 +134,6 @@ pub use self::{ }, }; -pub mod cli; -mod cucumber; -pub mod event; -pub mod feature; -pub mod parser; -pub mod runner; -pub mod step; -pub mod tag; -pub mod writer; - -#[cfg(feature = "macros")] -pub mod codegen; - /// Represents a shared user-defined state for a [Cucumber] run. /// It lives on per-[scenario][0] basis. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6bc2e1e8..eef5bd3a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12,6 +12,8 @@ //! //! [Gherkin]: https://cucumber.io/docs/gherkin/reference +pub mod basic; + use std::sync::Arc; use derive_more::{Display, Error}; @@ -23,8 +25,6 @@ use crate::feature::ExpandExamplesError; #[doc(inline)] pub use self::basic::Basic; -pub mod basic; - /// Source of parsed [`Feature`]s. /// /// [`Feature`]: gherkin::Feature From df487e1fbe87a7e4465475024e061a79c8f55d46 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 25 Oct 2021 20:03:26 +0300 Subject: [PATCH 21/21] Corrections --- CHANGELOG.md | 2 +- book/src/Features.md | 35 +++++------ codegen/Cargo.toml | 1 - codegen/src/lib.rs | 4 +- src/cli.rs | 137 +++++++++++++++++++++++++++++++----------- src/cucumber.rs | 139 ++++++++++++++++++------------------------- src/lib.rs | 1 + src/parser/basic.rs | 9 +-- src/runner/basic.rs | 31 +++++----- src/tag.rs | 9 +-- src/writer/basic.rs | 82 ++++++++++++------------- tests/wait.rs | 13 ++-- 12 files changed, 251 insertions(+), 212 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ead52d..b4093aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,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]) -- Complete `CLI` redesign ([#144]) +- Completely redesign and reworked CLI, making it composable and extendable. ([#144]) - [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept optional `&mut World` as their last parameter. ([#142]) ### Added diff --git a/book/src/Features.md b/book/src/Features.md index dc67dee2..68b05ea7 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -384,16 +384,14 @@ World::cucumber() ## CLI options -`Cucumber` provides several options that can be passed to the command-line. +Library provides several options that can be passed to the command-line. -Pass the `--help` option to print out all the available configuration options: - -``` +Use `--help` flag to print out all the available options: +```shell cargo test --test -- --help ``` Default output is: - ``` cucumber 0.10.0 Run the tests, pet a dog! @@ -404,32 +402,31 @@ USAGE: FLAGS: -h, --help Prints help information -V, --version Prints version information - --verbose Outputs Step's Doc String, if present + -v, --verbose Increased verbosity of an output: additionally 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 filter scenarios with [aliases: scenario-name] - -t, --tags Tag expression to filter scenarios with [aliases: scenario-tags] + --color Coloring policy for a console output [default: auto] + -i, --input Glob pattern to look for feature files with. By default, looks for `*.feature`s + in the path configured tests runner + -c, --concurrency Number of scenarios to run concurrently. If not specified, uses the value + configured in tests runner, or 64 by default + -n, --name Regex to filter scenarios by their name [aliases: scenario-name] + -t, --tags Tag expression to filter scenarios by [aliases: scenario-tags] ``` -Example with [tag expressions](https://cucumber.io/docs/cucumber/api/#tag-expressions) for filtering Scenarios: - -``` +Example with [tag expressions](https://cucumber.io/docs/cucumber/api#tag-expressions) for filtering `Scenario`s: +```shell cargo test --test -- --tags='@cat or @dog or @ferris' ``` -> Note: CLI overrides options set in the code. +> Note: CLI overrides any configurations 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`. +CLI options are designed to be composable from the one provided by [`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). -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. +You may also extend CLI options with custom ones, if you have such a need for running your tests. See a [`cli::Opts` example](https://docs.rs/cucumber/*/cucumber/cli/struct.Opts.html#example) for more details. diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 424a53a0..bb363e7c 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -31,7 +31,6 @@ syn = { version = "1.0.74", features = ["derive", "extra-traits", "full"] } [dev-dependencies] async-trait = "0.1" cucumber = { path = "..", features = ["macros"] } -futures = "0.3" tokio = { version = "1.12", features = ["macros", "rt-multi-thread", "time"] } [[test]] diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 9833d186..f5214a5b 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -150,7 +150,7 @@ macro_rules! step_attribute { /// - To use [`gherkin::Step`], name the argument as `step`, /// **or** mark the argument with a `#[step]` attribute. /// - /// ``` + /// ```rust /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; @@ -167,7 +167,7 @@ macro_rules! step_attribute { /// # Ok(Self {}) /// # } /// # } - /// + /// # /// #[given(regex = r"(\S+) is not (\S+)")] /// fn test_step( /// w: &mut MyWorld, diff --git a/src/cli.rs b/src/cli.rs index 0f78c55f..bab04650 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,29 +10,105 @@ //! 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]. +//! The main thing in this module is [`Opts`], which compose all the strongly +//! typed CLI options from [`Parser`], [`Runner`] and [`Writer`], and provide +//! filtering based on [`Regex`] or [tag expressions][1]. //! -//! [1]: https://cucumber.io/docs/cucumber/api/#tag-expressions +//! The idea behind this is that [`Parser`], [`Runner`] and/or [`Writer`] may +//! want to introduce their own CLI options to allow tweaking themselves, but we +//! still do want them combine in a single CLI and avoid any boilerplate burden. +//! +//! If the implementation doesn't need any CLI options, it may just use the +//! prepared [`cli::Empty`] stub. +//! +//! [`cli::Empty`]: self::Empty //! [`Parser`]: crate::Parser //! [`Runner`]: crate::Runner //! [`Writer`]: crate::Writer +//! [1]: https://cucumber.io/docs/cucumber/api#tag-expressions use gherkin::tagexpr::TagOperation; use regex::Regex; use structopt::StructOpt; -/// Run the tests, pet a dog!. -#[derive(Debug, StructOpt)] +// Workaround for overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr( + doc, + doc = r#" +Root CLI (command line interface) of a top-level [`Cucumber`] executor. + +It combines all the nested CLIs of [`Parser`], [`Runner`] and [`Writer`], +and may be extended with custom CLI options additionally. + +# Example + +```rust +# use std::{convert::Infallible, time::Duration}; +# +# use async_trait::async_trait; +# use cucumber::{cli, WorldInit}; +# use futures::FutureExt as _; +# use structopt::StructOpt; +# use tokio::time; +# +# #[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 { +#[derive(StructOpt)] +struct CustomOpts { + /// Additional time to wait in before hook. + #[structopt( + long, + parse(try_from_str = humantime::parse_duration) + )] + pre_pause: Option, +} + +let opts = cli::Opts::<_, _, _, CustomOpts>::from_args(); +let pre_pause = opts.custom.pre_pause.unwrap_or_default(); + +MyWorld::cucumber() + .before(move |_, _, _, _| time::sleep(pre_pause).boxed_local()) + .with_cli(opts) + .run_and_exit("tests/features/readme") + .await; +# }; +# +# tokio::runtime::Builder::new_current_thread() +# .enable_all() +# .build() +# .unwrap() +# .block_on(fut); +``` + +[`Cucumber`]: crate::Cucumber +[`Parser`]: crate::Parser +[`Runner`]: crate::Runner +[`Writer`]: crate::Writer +"# +)] +#[cfg_attr(not(doc), doc = "Run the tests, pet a dog!.")] +#[derive(Debug, Clone, StructOpt)] +#[structopt(name = "cucumber", about = "Run the tests, pet a dog!.")] pub struct Opts where - Custom: StructOpt, Parser: StructOpt, Runner: StructOpt, Writer: StructOpt, + Custom: StructOpt, { - /// Regex to filter scenarios with. + /// Regex to filter scenarios by their name. #[structopt( short = "n", long = "name", @@ -41,7 +117,7 @@ where )] pub re_filter: Option, - /// Tag expression to filter scenarios with. + /// Tag expression to filter scenarios by. #[structopt( short = "t", long = "tags", @@ -69,18 +145,18 @@ where #[structopt(flatten)] pub writer: Writer, - /// Custom CLI options. + /// Additional custom CLI options. #[structopt(flatten)] pub custom: Custom, } // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(doc, doc = "Empty CLI options.")] #[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 { /// This field exists only because [`StructOpt`] derive macro doesn't @@ -91,10 +167,6 @@ pub struct Empty { // 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 = r#" @@ -102,9 +174,8 @@ Composes two [`StructOpt`] derivers together. # Example -This struct is especially useful, when implementing custom [`Writer`], which -wraps another [`Writer`]. - +This struct is especially useful, when implementing custom [`Writer`] wrapping +another one: ```rust # use async_trait::async_trait; # use cucumber::{ @@ -134,12 +205,12 @@ where cli: &Self::Cli, ) { // Some custom logic including `cli.left.custom_option`. - + // ... self.0.handle_event(ev, &cli.right).await; } } -// useful blanket impls +// Useful blanket impls: #[async_trait(?Send)] impl<'val, W, Wr, Val> ArbitraryWriter<'val, W, Val> for CustomWriter @@ -177,29 +248,27 @@ where } ``` -[`Writer`]: crate::Writer"# +[`Writer`]: crate::Writer +"# +)] +#[cfg_attr( + not(doc), + allow(missing_docs, clippy::missing_docs_in_private_items) )] #[derive(Debug, StructOpt)] -pub struct Compose -where - L: StructOpt, - R: StructOpt, -{ - /// [`StructOpt`] deriver. +pub struct Compose { + /// Left [`StructOpt`] deriver. #[structopt(flatten)] pub left: L, - /// [`StructOpt`] deriver. + /// Right [`StructOpt`] deriver. #[structopt(flatten)] pub right: R, } -impl Compose -where - L: StructOpt, - R: StructOpt, -{ - /// Unpacks [`Compose`] into underlying `CLI`s. +impl Compose { + /// Unpacks this [`Compose`] into the underlying CLIs. + #[must_use] 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 b50b77da..0182bee6 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -39,8 +39,8 @@ use crate::{ /// provide [`Step`]s with [`WorldInit::collection()`] or by hand with /// [`Cucumber::given()`], [`Cucumber::when()`] and [`Cucumber::then()`]. /// -/// In case you want custom [`Parser`], [`Runner`] or [`Writer`] or -/// some other finer control, use [`Cucumber::custom()`] or +/// In case you want a custom [`Parser`], [`Runner`] or [`Writer`], or some +/// other finer control, use [`Cucumber::custom()`] or /// [`Cucumber::with_parser()`], [`Cucumber::with_runner()`] and /// [`Cucumber::with_writer()`] to construct your dream [Cucumber] executor! /// @@ -48,13 +48,13 @@ 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, + Cli: StructOpt, { /// [`Parser`] sourcing [`Feature`]s for execution. /// @@ -69,8 +69,11 @@ where /// [`Writer`] outputting [`event`]s to some output. writer: Wr, - /// `CLI` options. - cli: OptionalCli, + /// CLI options this [`Cucumber`] has been run with. + /// + /// If empty, then will be parsed from a command line. + #[allow(clippy::type_complexity)] // not really + cli: Option>, /// Type of the [`World`] this [`Cucumber`] run on. _world: PhantomData, @@ -79,18 +82,16 @@ where _parser_input: PhantomData, } -/// Type alias for [`Option`]`<`[`cli::Opts`]`>`. -type OptionalCli = Option>; - -impl Cucumber +impl Cucumber where W: World, P: Parser, R: Runner, Wr: Writer, - CCli: StructOpt, + Cli: StructOpt, { - /// Creates custom [`Cucumber`] executor. + /// Creates a custom [`Cucumber`] executor with the provided [`Parser`], + /// [`Runner`] and [`Writer`]. #[must_use] pub fn custom(parser: P, runner: R, writer: Wr) -> Self { Self { @@ -108,7 +109,7 @@ where pub fn with_parser( self, parser: NewP, - ) -> Cucumber + ) -> Cucumber where NewP: Parser, { @@ -128,7 +129,7 @@ where pub fn with_runner( self, runner: NewR, - ) -> Cucumber + ) -> Cucumber where NewR: Runner, { @@ -148,7 +149,7 @@ where pub fn with_writer( self, writer: NewWr, - ) -> Cucumber + ) -> Cucumber where NewWr: Writer, { @@ -174,12 +175,11 @@ where /// async data-autoplay="true" data-rows="17"> /// /// - /// Adjust [`Cucumber`] to re-output all [`Skipped`] steps at the end: + /// Adjust [`Cucumber`] to re-output all the [`Skipped`] steps at the end: /// ```rust /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] @@ -214,7 +214,7 @@ where #[must_use] pub fn repeat_skipped( self, - ) -> Cucumber, CCli> { + ) -> Cucumber, Cli> { Cucumber { parser: self.parser, runner: self.runner, @@ -234,7 +234,6 @@ where /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] @@ -264,12 +263,11 @@ 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 the [`Failed`] steps at the end: /// ```rust,should_panic /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] @@ -313,7 +311,7 @@ where #[must_use] pub fn repeat_failed( self, - ) -> Cucumber, CCli> { + ) -> Cucumber, Cli> { Cucumber { parser: self.parser, runner: self.runner, @@ -324,7 +322,7 @@ where } } - /// Re-output steps by the given `filter` predicate. + /// Re-outputs steps by the given `filter` predicate. /// /// # Example /// @@ -363,13 +361,12 @@ where /// async data-autoplay="true" data-rows="21"> /// /// - /// Adjust [`Cucumber`] to re-output all [`Failed`] steps ta the end by + /// Adjust [`Cucumber`] to re-output all the [`Failed`] steps ta the end by /// providing a custom `filter` predicate: /// ```rust,should_panic /// # use std::convert::Infallible; /// # /// # use async_trait::async_trait; - /// # use futures::FutureExt as _; /// # use cucumber::WorldInit; /// # /// # #[derive(Debug, WorldInit)] @@ -435,7 +432,7 @@ where pub fn repeat_if( self, filter: F, - ) -> Cucumber, CCli> + ) -> Cucumber, Cli> where F: Fn(&parser::Result>) -> bool, { @@ -450,13 +447,13 @@ where } } -impl Cucumber +impl Cucumber where W: World, P: Parser, R: Runner, Wr: Writer + for<'val> writer::Arbitrary<'val, W, String>, - CCli: StructOpt, + Cli: StructOpt, { /// Consider [`Skipped`] steps as [`Failed`] if their [`Scenario`] isn't /// marked with `@allow_skipped` tag. @@ -478,7 +475,6 @@ where /// # /// # use async_trait::async_trait; /// # use cucumber::WorldInit; - /// # use futures::FutureExt as _; /// # /// # #[derive(Debug, WorldInit)] /// # struct MyWorld; @@ -530,7 +526,7 @@ where #[must_use] pub fn fail_on_skipped( self, - ) -> Cucumber, CCli> { + ) -> Cucumber, Cli> { Cucumber { parser: self.parser, runner: self.runner, @@ -554,7 +550,7 @@ where /// /// /// Adjust [`Cucumber`] to fail on all [`Skipped`] steps, but the ones - /// marked with `@dog` tag: + /// marked with a `@dog` tag: /// ```rust,should_panic /// # use std::convert::Infallible; /// # @@ -624,7 +620,7 @@ where pub fn fail_on_skipped_with( self, filter: Filter, - ) -> Cucumber, CCli> + ) -> Cucumber, Cli> where Filter: Fn( &gherkin::Feature, @@ -643,13 +639,13 @@ where } } -impl Cucumber +impl Cucumber where W: World, P: Parser, R: Runner, Wr: Writer, - CCli: StructOpt + StructOptInternal, + Cli: StructOpt + StructOptInternal, { /// Runs [`Cucumber`]. /// @@ -661,11 +657,18 @@ where self.filter_run(input, |_, _, _| true).await } - /// Provides custom `CLI` options. + /// Consumes already parsed [`cli::Opts`]. + /// + /// This method allows to pre-parse [`cli::Opts`] for custom needs before + /// using them inside [`Cucumber`]. + /// + /// Also, any additional custom CLI options may be specified as a + /// [`StructOpt`] deriving type, used as the last type parameter of + /// [`cli::Opts`]. /// - /// This method exists not to hijack console and give users an ability to - /// compose custom `CLI` by providing [`StructOpt`] deriver as last generic - /// parameter in [`cli::Opts`]. + /// > ⚠️ __WARNING__: Any CLI options of [`Parser`], [`Runner`], [`Writer`] + /// or custom ones should not overlap, otherwise + /// [`cli::Opts`] will fail to parse on startup. /// /// # Example /// @@ -692,8 +695,8 @@ where /// # /// # let fut = async { /// #[derive(StructOpt)] - /// struct Cli { - /// /// Time to wait in before hook. + /// struct CustomCli { + /// /// Additional time to wait in a before hook. /// #[structopt( /// long, /// parse(try_from_str = humantime::parse_duration) @@ -701,7 +704,7 @@ where /// before_time: Option, /// } /// - /// let cli = cli::Opts::<_, _, _, Cli>::from_args(); + /// let cli = cli::Opts::<_, _, _, CustomCli>::from_args(); /// let time = cli.custom.before_time.unwrap_or_default(); /// /// MyWorld::cucumber() @@ -731,34 +734,7 @@ 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] - /// ``` + /// Also, specifying `--help` flag will describe `--before-time` now. /// /// [`Feature`]: gherkin::Feature pub fn with_cli( @@ -774,7 +750,6 @@ where writer, .. } = self; - Cucumber { parser, runner, @@ -914,13 +889,13 @@ where } } -impl Debug for Cucumber +impl Debug for Cucumber where W: World, P: Debug + Parser, R: Debug + Runner, Wr: Debug + Writer, - CCli: StructOpt, + Cli: StructOpt, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Cucumber") @@ -985,12 +960,12 @@ where } } -impl Cucumber +impl Cucumber where W: World, R: Runner, Wr: Writer, - CCli: StructOpt, + Cli: StructOpt, I: AsRef, { /// Sets the provided language of [`gherkin`] files. @@ -1007,13 +982,13 @@ where } } -impl - Cucumber, Wr, CCli> +impl + Cucumber, Wr, Cli> where W: World, P: Parser, Wr: Writer, - CCli: StructOpt, + Cli: StructOpt, F: Fn( &gherkin::Feature, Option<&gherkin::Rule>, @@ -1058,7 +1033,7 @@ where pub fn which_scenario( self, func: Which, - ) -> Cucumber, Wr, CCli> + ) -> Cucumber, Wr, Cli> where Which: Fn( &gherkin::Feature, @@ -1094,7 +1069,7 @@ where pub fn before( self, func: Before, - ) -> Cucumber, Wr, CCli> + ) -> Cucumber, Wr, Cli> where Before: for<'a> Fn( &'a gherkin::Feature, @@ -1141,7 +1116,7 @@ where pub fn after( self, func: After, - ) -> Cucumber, Wr, CCli> + ) -> Cucumber, Wr, Cli> where After: for<'a> Fn( &'a gherkin::Feature, @@ -1206,13 +1181,13 @@ where } } -impl Cucumber +impl Cucumber where W: World, P: Parser, R: Runner, Wr: FailureWriter, - CCli: StructOpt + StructOptInternal, + Cli: StructOpt + StructOptInternal, { /// Runs [`Cucumber`]. /// diff --git a/src/lib.rs b/src/lib.rs index 0e1dad95..22e5219e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,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/parser/basic.rs b/src/parser/basic.rs index 1a0febf5..b8294eaf 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -30,15 +30,16 @@ 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(doc, doc = "CLI options of a [`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")] + /// Glob pattern to look for feature files with. By default, looks for + /// `*.feature`s in the path configured tests runner. + #[structopt(long = "input", short = "i", name = "glob")] pub features: Option, } @@ -172,7 +173,7 @@ pub struct UnsupportedLanguageError( #[error(not(source))] pub Cow<'static, str>, ); -/// Wrapper over [`GlobWalker`] with a [`FromStr`] impl. +/// Wrapper over [`GlobWalker`] implementing a [`FromStr`]. pub struct Walker(GlobWalker); impl fmt::Debug for Walker { diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 6f67f4cb..7a10499b 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -42,6 +42,21 @@ use crate::{ parser, step, Runner, Step, World, }; +// Workaround for overwritten doc-comments. +// https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(doc, doc = "CLI options of a [`Basic`] [`Runner`].")] +#[cfg_attr( + not(doc), + allow(clippy::missing_docs_in_private_items, missing_docs) +)] +#[derive(Clone, Copy, Debug, StructOpt)] +pub struct Cli { + /// Number of scenarios to run concurrently. If not specified, uses the + /// value configured in tests runner, or 64 by default. + #[structopt(long, short, name = "int")] + pub concurrency: Option, +} + /// Type determining whether [`Scenario`]s should run concurrently or /// sequentially. /// @@ -142,20 +157,6 @@ pub struct Basic< after_hook: 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`] [`Runner`].")] -#[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 { @@ -411,7 +412,7 @@ where ); let execute = execute( buffer, - cli.concurrent.or(max_concurrent_scenarios), + cli.concurrency.or(max_concurrent_scenarios), steps, sender, before_hook, diff --git a/src/tag.rs b/src/tag.rs index 4f52be64..27ec75c4 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -8,15 +8,16 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! [`TagOperation`] extension. +//! Extension of a [`TagOperation`]. use gherkin::tagexpr::TagOperation; use sealed::sealed; -/// Helper method to evaluate [`TagOperation`]. +/// Extension of a [`TagOperation`] allowing to evaluate it. #[sealed] pub trait Ext { - /// Evaluates [`TagOperation`]. + /// Evaluates this [`TagOperation`] for the given `tags`. + #[must_use] fn eval(&self, tags: &[String]) -> bool; } @@ -26,7 +27,7 @@ impl Ext for TagOperation { match self { 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::Not(t) => !t.eval(tags), Self::Tag(t) => tags.iter().any(|tag| tag == t), } } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index 1d41c3c7..34eb99b4 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -32,64 +32,40 @@ use crate::{ ArbitraryWriter, World, Writer, }; -/// Default [`Writer`] implementation outputting to [`Term`]inal (STDOUT by -/// default). -/// -/// Pretty-prints with colors if terminal was successfully detected, otherwise -/// has simple output. Useful for running tests with CI tools. -#[derive(Debug)] -pub struct Basic { - /// Terminal to write the output into. - terminal: Term, - - /// [`Styles`] for terminal output. - styles: Styles, - - /// Current indentation that events are outputted with. - indent: usize, - - /// Number of lines to clear. - lines_to_clear: usize, -} - // Workaround for overwritten doc-comments. // https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(doc, doc = "CLI options of a [`Basic`] [`Writer`].")] #[cfg_attr( not(doc), allow(missing_docs, clippy::missing_docs_in_private_items) )] -#[cfg_attr(doc, doc = "CLI options of [`Basic`] [`Writer`].")] #[derive(Clone, Copy, Debug, StructOpt)] pub struct Cli { - /// Outputs Step's Doc String, if present. - #[structopt(long)] + /// Increased verbosity of an output: additionally outputs step's doc + /// string (if present). + #[structopt(long, short)] pub verbose: bool, - /// Indicates, whether output should be colored or not. - #[structopt( - long, - short, - name = "auto|always|never", - default_value = "auto" - )] - pub colors: Colors, + /// Coloring policy for a console output. + #[structopt(long, name = "auto|always|never", default_value = "auto")] + pub color: Coloring, } -/// Indicates, whether output should be colored or not. +/// Possible policies of a [`console`] output coloring. #[derive(Clone, Copy, Debug)] -pub enum Colors { - /// Lets [`console::colors_enabled()`] to decide, whether output should be - /// colored or not. +pub enum Coloring { + /// Letting [`console::colors_enabled()`] to decide, whether output should + /// be colored. Auto, - /// Forces colored output. + /// Forcing of a colored output. Always, - /// Forces basic output. + /// Forcing of a non-colored output. Never, } -impl FromStr for Colors { +impl FromStr for Coloring { type Err = &'static str; fn from_str(s: &str) -> Result { @@ -102,6 +78,26 @@ impl FromStr for Colors { } } +/// Default [`Writer`] implementation outputting to [`Term`]inal (STDOUT by +/// default). +/// +/// Pretty-prints with colors if terminal was successfully detected, otherwise +/// has simple output. Useful for running tests with CI tools. +#[derive(Debug)] +pub struct Basic { + /// Terminal to write the output into. + terminal: Term, + + /// [`Styles`] for terminal output. + styles: Styles, + + /// Current indentation that events are outputted with. + indent: usize, + + /// Number of lines to clear. + lines_to_clear: usize, +} + #[async_trait(?Send)] impl Writer for Basic { type Cli = Cli; @@ -114,10 +110,10 @@ impl Writer for Basic { ) { use event::{Cucumber, Feature}; - match cli.colors { - Colors::Always => self.styles.is_present = true, - Colors::Never => self.styles.is_present = false, - Colors::Auto => {} + match cli.color { + Coloring::Always => self.styles.is_present = true, + Coloring::Never => self.styles.is_present = false, + Coloring::Auto => {} }; match ev { @@ -564,7 +560,7 @@ impl Basic { step.position.col, format_str_with_indent( format!("{}", err), - self.indent.saturating_sub(3) + 3 + self.indent.saturating_sub(3) + 3, ), world .map(|w| format_str_with_indent( diff --git a/tests/wait.rs b/tests/wait.rs index 82ea28b6..1ee780c7 100644 --- a/tests/wait.rs +++ b/tests/wait.rs @@ -7,30 +7,29 @@ use structopt::StructOpt; use tokio::time; #[derive(StructOpt)] -struct Cli { - /// Time to wait in before and after hooks. +struct CustomCli { + /// Additional time to wait in before and after hooks. #[structopt( long, default_value = "10ms", parse(try_from_str = humantime::parse_duration) )] - time: Duration, + pause: Duration, } #[tokio::main] async fn main() { - let cli = cli::Opts::<_, _, _, Cli>::from_args(); + let cli = cli::Opts::<_, _, _, CustomCli>::from_args(); - let time = cli.custom.time; let res = World::cucumber() .before(move |_, _, _, w| { async move { w.0 = 0; - time::sleep(time).await; + time::sleep(cli.custom.pause).await; } .boxed_local() }) - .after(move |_, _, _, _| time::sleep(time).boxed_local()) + .after(move |_, _, _, _| time::sleep(cli.custom.pause).boxed_local()) .with_cli(cli) .run_and_exit("tests/features/wait");