diff --git a/CHANGELOG.md b/CHANGELOG.md index d3a2742a..9afc3ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,16 @@ All user visible changes to `cucumber` crate will be documented in this file. Th ### BC Breaks -- Bump up [MSRV] to 1.61 for more clever support of [Cargo feature]s and simplified codegen. +- Bump up [MSRV] to 1.61 for more clever support of [Cargo feature]s and simplified codegen. ([fbd08ec2], [cf055ac0]) + +### Changed + +- Provided default CLI options are now global (allowed to be specified after custom subcommands). ([#216], [#215]) + +[#215]: /../../issues/215 +[#216]: /../../pull/216 +[cf055ac0]: /../../commit/cf055ac06c7b72f572882ce15d6a60da92ad60a0 +[fbd08ec2]: /../../commit/fbd08ec24dbd036c89f5f0af4d936b616790a166 diff --git a/book/src/cli.md b/book/src/cli.md index 61707ee3..63f48b91 100644 --- a/book/src/cli.md +++ b/book/src/cli.md @@ -152,6 +152,123 @@ async fn main() { +## Aliasing + +[Cargo alias] is a neat way to define shortcuts for regularly used customized tests running commands. + +```rust +# use std::{convert::Infallible, time::Duration}; +# +# use async_trait::async_trait; +# use cucumber::{cli, given, then, when, World, WorldInit}; +# use futures::FutureExt as _; +# use tokio::time::sleep; +# +# #[derive(Debug, Default)] +# struct Animal { +# pub hungry: bool, +# } +# +# impl Animal { +# fn feed(&mut self) { +# self.hungry = false; +# } +# } +# +# #[derive(Debug, WorldInit)] +# pub struct AnimalWorld { +# cat: Animal, +# } +# +# #[async_trait(?Send)] +# impl World for AnimalWorld { +# type Error = Infallible; +# +# async fn new() -> Result { +# Ok(Self { +# cat: Animal::default(), +# }) +# } +# } +# +# #[given(regex = r"^a (hungry|satiated) cat$")] +# async fn hungry_cat(world: &mut AnimalWorld, state: String) { +# match state.as_str() { +# "hungry" => world.cat.hungry = true, +# "satiated" => world.cat.hungry = false, +# _ => unreachable!(), +# } +# } +# +# #[when("I feed the cat")] +# async fn feed_cat(world: &mut AnimalWorld) { +# world.cat.feed(); +# } +# +# #[then("the cat is not hungry")] +# async fn cat_is_fed(world: &mut AnimalWorld) { +# assert!(!world.cat.hungry); +# } +# +#[derive(clap::Args)] +struct CustomOpts { + #[clap(subcommand)] + command: Option, +} + +#[derive(clap::Subcommand)] +enum SubCommand { + Smoke(Smoke), +} + +#[derive(clap::Args)] +struct Smoke { + /// Additional time to wait in before hook. + #[clap( + long, + parse(try_from_str = humantime::parse_duration) + )] + pre_pause: Option, +} + +#[tokio::main] +async fn main() { + let opts = cli::Opts::<_, _, _, CustomOpts>::parsed(); + + let pre_pause = if let Some(SubCommand::Smoke(Smoke { pre_pause })) = + opts.custom.command + { + pre_pause + } else { + None + } + .unwrap_or_default(); + + AnimalWorld::cucumber() + .before(move |_, _, _, _| sleep(pre_pause).boxed_local()) + .with_cli(opts) + .run_and_exit("tests/features/book/cli.feature") + .await; +} +``` + +The alias should be specified in `.cargo/config.toml` file of the project: +```yaml +[alias] +smoke = "test -p cucumber --test cli -- smoke --pre-pause=5s -vv --fail-fast" +``` + +Now it can be used as: +```bash +cargo smoke +cargo smoke --tags=@hungry +``` + +> __NOTE__: The default CLI options may be specified after a custom subcommand, because they are defined as [global][1] ones. This may be applied to custom CLI options too, if necessary. + + + + [`cli::Compose`]: https://docs.rs/cucumber/*/cucumber/cli/struct.Compose.html [`cli::Empty`]: https://docs.rs/cucumber/*/cucumber/cli/struct.Empty.html [`cucumber`]: https://docs.rs/cucumber @@ -162,3 +279,7 @@ async fn main() { [`Runner::Cli`]: https://docs.rs/cucumber/*/cucumber/trait.Runner.html#associatedtype.Cli [`Writer`]: architecture/writer.md [`Writer::Cli`]: https://docs.rs/cucumber/*/cucumber/trait.Writer.html#associatedtype.Cli + +[Cargo alias]: https://doc.rust-lang.org/cargo/reference/config.html#alias + +[1]: https://docs.rs/clap/latest/clap/struct.Arg.html#method.global diff --git a/codegen/CHANGELOG.md b/codegen/CHANGELOG.md index fcd9f56b..1dc5ee28 100644 --- a/codegen/CHANGELOG.md +++ b/codegen/CHANGELOG.md @@ -13,7 +13,10 @@ All user visible changes to `cucumber-codegen` crate will be documented in this ### BC Breaks -- Bump up [MSRV] to 1.61 for more clever support of [Cargo feature]s and simplified codegen. +- Bump up [MSRV] to 1.61 for more clever support of [Cargo feature]s and simplified codegen. ([fbd08ec2], [cf055ac0]) + +[cf055ac0]: /../../commit/cf055ac06c7b72f572882ce15d6a60da92ad60a0 +[fbd08ec2]: /../../commit/fbd08ec24dbd036c89f5f0af4d936b616790a166 diff --git a/src/cli.rs b/src/cli.rs index cb24eca7..0b4fa80e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -106,7 +106,8 @@ where short = 'n', long = "name", name = "regex", - visible_alias = "scenario-name" + visible_alias = "scenario-name", + global = true )] pub re_filter: Option, @@ -118,7 +119,8 @@ where short = 't', long = "tags", name = "tagexpr", - conflicts_with = "regex" + conflicts_with = "regex", + global = true )] pub tags_filter: Option, diff --git a/src/parser/basic.rs b/src/parser/basic.rs index 14887c41..39446e6f 100644 --- a/src/parser/basic.rs +++ b/src/parser/basic.rs @@ -33,7 +33,7 @@ use super::{Error as ParseError, Parser}; pub struct Cli { /// Glob pattern to look for feature files with. By default, looks for /// `*.feature`s in the path configured tests runner. - #[clap(long = "input", short = 'i', name = "glob")] + #[clap(long = "input", short = 'i', name = "glob", global = true)] pub features: Option, } diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 38ff2acd..01cd71a2 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -45,11 +45,11 @@ use crate::{ pub struct Cli { /// Number of scenarios to run concurrently. If not specified, uses the /// value configured in tests runner, or 64 by default. - #[clap(long, short, name = "int")] + #[clap(long, short, name = "int", global = true)] pub concurrency: Option, /// Run tests until the first failure. - #[clap(long)] + #[clap(long, global = true)] pub fail_fast: bool, } diff --git a/src/writer/basic.rs b/src/writer/basic.rs index f3632a2c..eccb3ab0 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -42,11 +42,16 @@ pub struct Cli { /// /// `-v` is default verbosity, `-vv` additionally outputs world on failed /// steps, `-vvv` additionally outputs step's doc string (if present). - #[clap(short, parse(from_occurrences))] + #[clap(short, parse(from_occurrences), global = true)] pub verbose: u8, /// Coloring policy for a console output. - #[clap(long, name = "auto|always|never", default_value = "auto")] + #[clap( + long, + name = "auto|always|never", + default_value = "auto", + global = true + )] pub color: Coloring, } diff --git a/src/writer/junit.rs b/src/writer/junit.rs index 3f64228e..a334c811 100644 --- a/src/writer/junit.rs +++ b/src/writer/junit.rs @@ -43,7 +43,7 @@ pub struct Cli { /// /// `0` is default verbosity, `1` additionally outputs world on failed /// steps. - #[clap(long = "junit-v", name = "0|1")] + #[clap(long = "junit-v", name = "0|1", global = true)] pub verbose: Option, } diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 00000000..4c12c6d3 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,113 @@ +use std::{convert::Infallible, panic::AssertUnwindSafe}; + +use async_trait::async_trait; +use clap::Parser; +use cucumber::{cli, given, WorldInit}; +use futures::FutureExt as _; + +#[derive(cli::Args)] +struct CustomCli { + #[clap(subcommand)] + command: Option, +} + +#[derive(clap::Subcommand)] +enum SubCommand { + Smoke(Smoke), +} + +#[derive(cli::Args)] +struct Smoke { + #[clap(long)] + report_name: String, +} + +#[derive(Clone, Copy, Debug, WorldInit)] +struct World; + +#[async_trait(?Send)] +impl cucumber::World for World { + type Error = Infallible; + + async fn new() -> Result { + Ok(World) + } +} + +#[given("an invalid step")] +fn invalid_step(_world: &mut World) { + assert!(false); +} + +// This test uses a subcommand with the global option `--tags` to filter on two +// failing tests and verifies that the error output contains 2 failing steps. +#[tokio::test] +async fn tags_option_filters_all_scenarios_with_subcommand() { + let cli = cli::Opts::<_, _, _, CustomCli>::try_parse_from(&[ + "test", + "smoke", + r#"--report-name="smoke.report""#, + "--tags=@all", + ]) + .expect("Invalid command line"); + + let res = World::cucumber() + .with_cli(cli) + .run_and_exit("tests/features/cli"); + + let err = AssertUnwindSafe(res) + .catch_unwind() + .await + .expect_err("should err"); + let err = err.downcast_ref::().unwrap(); + + assert_eq!(err, "2 steps failed"); +} + +// This test uses a subcommand with the global option `--tags` to filter on one +// failing test and verifies that the error output contains 1 failing step. +#[tokio::test] +async fn tags_option_filters_scenario1_with_subcommand() { + let cli = cli::Opts::<_, _, _, CustomCli>::try_parse_from(&[ + "test", + "smoke", + r#"--report-name="smoke.report""#, + "--tags=@scenario-1", + ]) + .expect("Invalid command line"); + + let res = World::cucumber() + .with_cli(cli) + .run_and_exit("tests/features/cli"); + + let err = AssertUnwindSafe(res) + .catch_unwind() + .await + .expect_err("should err"); + let err = err.downcast_ref::().unwrap(); + + assert_eq!(err, "1 step failed"); +} + +// This test verifies that the global option `--tags` is still available without +// subcommands and that the error output contains 1 failing step. +#[tokio::test] +async fn tags_option_filters_scenario1_no_subcommand() { + let cli = cli::Opts::<_, _, _, CustomCli>::try_parse_from(&[ + "test", + "--tags=@scenario-1", + ]) + .expect("Invalid command line"); + + let res = World::cucumber() + .with_cli(cli) + .run_and_exit("tests/features/cli"); + + let err = AssertUnwindSafe(res) + .catch_unwind() + .await + .expect_err("should err"); + let err = err.downcast_ref::().unwrap(); + + assert_eq!(err, "1 step failed"); +} diff --git a/tests/features/cli/subcomand_global_option.feature b/tests/features/cli/subcomand_global_option.feature new file mode 100644 index 00000000..b82e92d9 --- /dev/null +++ b/tests/features/cli/subcomand_global_option.feature @@ -0,0 +1,10 @@ +Feature: Global option `--tags` with subcommands + + @scenario-1 @all + Scenario: Two invalid steps + Given an invalid step + And an invalid step + + @scenario-2 @all + Scenario: One invalid step + Given an invalid step