From 05d8e18d873919517c28ec623a4356ffd645039e Mon Sep 17 00:00:00 2001 From: ilslv Date: Wed, 10 Nov 2021 13:40:30 +0300 Subject: [PATCH 1/9] Support Cucumber Expressions --- book/src/Features.md | 4 +- book/src/Getting_Started.md | 59 ++++++++++++++++++++++ book/src/Test_Modules_Organization.md | 2 +- codegen/Cargo.toml | 2 + codegen/src/attribute.rs | 69 +++++++++++++++++++------- codegen/src/lib.rs | 21 +++++++- codegen/tests/example.rs | 10 ++-- codegen/tests/features/example.feature | 8 +-- 8 files changed, 143 insertions(+), 32 deletions(-) diff --git a/book/src/Features.md b/book/src/Features.md index a1013a4b..80967c43 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -173,7 +173,7 @@ async fn cat_is_fed(world: &mut AnimalWorld) { -### Combining `regex` and `FromStr` +### Combining `regex`/`cucumber-expressions` and `FromStr` At parsing stage, `` are replaced by value from cells. That means you can parse table cells into any type, that implements [`FromStr`](https://doc.rust-lang.org/stable/std/str/trait.FromStr.html). @@ -262,7 +262,7 @@ async fn feed_cat(world: &mut AnimalWorld, times: usize) { } } -#[then(regex = r"^the (\S+) is not hungry$")] +#[then(expr = "the {word} is not hungry")] async fn cat_is_fed(world: &mut AnimalWorld) { sleep(Duration::from_secs(2)).await; diff --git a/book/src/Getting_Started.md b/book/src/Getting_Started.md index e560fa10..85750fd1 100644 --- a/book/src/Getting_Started.md +++ b/book/src/Getting_Started.md @@ -360,6 +360,64 @@ We surround regex with `^..$` to ensure the __exact__ match. This is much more u Captured groups are __bold__ to indicate which part of step could be dynamically changed. +[Cucumber Expressions] are supported too! +```rust +# use std::convert::Infallible; +# +# use async_trait::async_trait; +# use cucumber::{given, then, when, World, WorldInit}; +# +# #[derive(Debug)] +# struct Cat { +# pub hungry: bool, +# } +# +# impl Cat { +# fn feed(&mut self) { +# self.hungry = false; +# } +# } +# +# #[derive(Debug, WorldInit)] +# pub struct AnimalWorld { +# cat: Cat, +# } +# +# #[async_trait(?Send)] +# impl World for AnimalWorld { +# type Error = Infallible; +# +# async fn new() -> Result { +# Ok(Self { +# cat: Cat { hungry: false }, +# }) +# } +# } +# +#[given(expr = "a {word} cat")] +fn hungry_cat(world: &mut AnimalWorld, state: String) { + match state.as_str() { + "hungry" => world.cat.hungry = true, + "satiated" => world.cat.hungry = false, + s => panic!("expected 'hungry' or 'satiated', found: {}", s), + } +} +# +# #[when("I feed the cat")] +# fn feed_cat(world: &mut AnimalWorld) { +# world.cat.feed(); +# } +# +# #[then("the cat is not hungry")] +# fn cat_is_fed(world: &mut AnimalWorld) { +# assert!(!world.cat.hungry); +# } +# +# fn main() { +# futures::executor::block_on(AnimalWorld::run("/tests/features/book")); +# } +``` + A contrived example, but this demonstrates that steps can be reused as long as they are sufficiently precise in both their description and implementation. If, for example, the wording for our `Then` step was `The cat is no longer hungry`, it'd imply something about the expected initial state, when that is not the purpose of a `Then` step, but rather of the `Given` step.
@@ -540,4 +598,5 @@ Feature: Animal feature [Cucumber]: https://cucumber.io +[Cucumber Expressions]: https://docs.rs/cucumber-expressions [Gherkin]: https://cucumber.io/docs/gherkin/reference diff --git a/book/src/Test_Modules_Organization.md b/book/src/Test_Modules_Organization.md index 0f2f6a6a..69b7271d 100644 --- a/book/src/Test_Modules_Organization.md +++ b/book/src/Test_Modules_Organization.md @@ -19,7 +19,7 @@ Of course, how you group your step definitions is really up to you and your team ## Avoid duplication -Avoid writing similar step definitions, as they can lead to clutter. While documenting your steps helps, making use of [`regex` and `FromStr`](Features.md#combining-regex-and-fromstr) can do wonders. +Avoid writing similar step definitions, as they can lead to clutter. While documenting your steps helps, making use of [`regex`/`cucumber-expressions` and `FromStr`](Features.md#combining-regexcucumber-expressions-and-fromstr) can do wonders. diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 2281830d..645f5cf6 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -21,6 +21,8 @@ exclude = ["/tests/"] proc-macro = true [dependencies] +# TODO: switch to crates.io +cucumber-expressions = { git = "https://github.com/ilslv/cucumber-expressions-1.git" } inflections = "1.1" itertools = "0.10" proc-macro2 = "1.0.28" diff --git a/codegen/src/attribute.rs b/codegen/src/attribute.rs index 6a65c448..506eaf1d 100644 --- a/codegen/src/attribute.rs +++ b/codegen/src/attribute.rs @@ -12,6 +12,7 @@ use std::mem; +use cucumber_expressions::Expression; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use regex::{self, Regex}; @@ -176,10 +177,13 @@ impl Step { fn fn_arguments_and_additional_parsing( &self, ) -> syn::Result<(TokenStream, Option)> { - let is_regex = matches!(self.attr_arg, AttributeArgument::Regex(_)); + let is_regex_or_expr = matches!( + self.attr_arg, + AttributeArgument::Regex(_) | AttributeArgument::Expression(_), + ); let func = &self.func; - if is_regex { + if is_regex_or_expr { if let Some(elem_ty) = find_first_slice(&func.sig) { let addon_parsing = Some(quote! { let __cucumber_matches = __cucumber_ctx @@ -328,6 +332,9 @@ enum AttributeArgument { /// `#[step(regex = "regex")]` case. Regex(syn::LitStr), + + /// `#[step(expr = "cucumber-expression")]` case. + Expression(syn::LitStr), } impl AttributeArgument { @@ -336,7 +343,7 @@ impl AttributeArgument { /// [`syn::LitStr`]: struct@syn::LitStr fn regex_literal(&self) -> syn::LitStr { match self { - Self::Regex(l) => l.clone(), + Self::Regex(l) | Self::Expression(l) => l.clone(), Self::Literal(l) => syn::LitStr::new( &format!("^{}$", regex::escape(&l.value())), l.span(), @@ -350,21 +357,45 @@ impl Parse for AttributeArgument { let arg = input.parse::()?; match arg { syn::NestedMeta::Meta(syn::Meta::NameValue(arg)) => { - if arg.path.is_ident("regex") { - let str_lit = to_string_literal(arg.lit)?; - - drop(Regex::new(str_lit.value().as_str()).map_err( - |e| { - syn::Error::new( - str_lit.span(), - format!("Invalid regex: {}", e), - ) - }, - )?); - - Ok(Self::Regex(str_lit)) - } else { - Err(syn::Error::new(arg.span(), "Expected regex argument")) + match arg.path.get_ident() { + Some(i) if i == "regex" => { + let str_lit = to_string_literal(arg.lit)?; + + drop(Regex::new(str_lit.value().as_str()).map_err( + |e| { + syn::Error::new( + str_lit.span(), + format!("Invalid regex: {}", e), + ) + }, + )?); + + Ok(Self::Regex(str_lit)) + } + Some(i) if i == "expr" || i == "expression" => { + let str_lit = to_string_literal(arg.lit)?; + + let expression_regex = + Expression::regex(str_lit.value().as_str()) + .map_err(|e| { + syn::Error::new( + str_lit.span(), + format!( + "Invalid cucumber expression: {}", + e, + ), + ) + })?; + + Ok(Self::Expression(syn::LitStr::new( + expression_regex.as_str(), + str_lit.span(), + ))) + } + _ => Err(syn::Error::new( + arg.span(), + "Expected 'regex' or 'expr' argument", + )), } } @@ -372,7 +403,7 @@ impl Parse for AttributeArgument { syn::NestedMeta::Meta(_) => Err(syn::Error::new( arg.span(), - "Expected string literal or regex argument", + "Expected string literal, 'regex' or 'expr' argument", )), } } diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 4b339b06..a879f71e 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -115,7 +115,7 @@ macro_rules! step_attribute { /// # use std::{convert::Infallible}; /// # /// # use async_trait::async_trait; - /// use cucumber::{given, World, WorldInit}; + /// use cucumber::{given, when, World, WorldInit}; /// /// #[derive(Debug, WorldInit)] /// struct MyWorld; @@ -130,6 +130,7 @@ macro_rules! step_attribute { /// } /// /// #[given(regex = r"(\S+) is (\d+)")] + /// #[when(expr = "{word} is {int}")] /// fn test(w: &mut MyWorld, param: String, num: i32) { /// assert_eq!(param, "foo"); /// assert_eq!(num, 0); @@ -141,6 +142,22 @@ macro_rules! step_attribute { /// } /// ``` /// + /// # Attributes + /// + /// - `#[given(regex = "regex")]` + /// + /// Uses [`Regex`], which correctness is checked at compile time. + /// + /// - `#[given(expression = "cucumber-expression")]` or + /// `#[given(expr = "cucumber-expression")]` + /// + /// Uses [`cucumber-expressions`][1], which correctness is checked at + /// compile time. + /// + /// - `#[given("literal")]` + /// + /// Matches only **exact** literal. + /// /// # Arguments /// /// - First argument has to be mutable reference to the [`WorldInit`] @@ -191,8 +208,10 @@ macro_rules! step_attribute { /// to implement [`Display`], so returning it will cause the step to /// fail. /// + /// [1]: cucumber_expressions /// [`Display`]: std::fmt::Display /// [`FromStr`]: std::str::FromStr + /// [`Regex`]: regex::Regex /// [`gherkin::Step`]: https://bit.ly/3j42hcd /// [`World`]: https://bit.ly/3j0aWw7 #[proc_macro_attribute] diff --git a/codegen/tests/example.rs b/codegen/tests/example.rs index aa592e7c..dae24aef 100644 --- a/codegen/tests/example.rs +++ b/codegen/tests/example.rs @@ -39,7 +39,7 @@ async fn test_non_regex_async(w: &mut MyWorld, #[step] ctx: &Step) { } #[given(regex = r"(\S+) is (\d+)")] -#[when(regex = r"(\S+) is (\d+)")] +#[when(expression = r"{word} is {int}")] async fn test_regex_async( w: &mut MyWorld, step: String, @@ -64,7 +64,7 @@ fn test_regex_sync_slice(w: &mut MyWorld, step: &Step, matches: &[String]) { w.foo += 1; } -#[when(regex = r#"^I write "(\S+)" to `([^`\s]+)`$"#)] +#[when(regex = r#"^I write "(\S+)" to '([^'\s]+)'$"#)] fn test_return_result_write( w: &mut MyWorld, what: String, @@ -75,16 +75,16 @@ fn test_return_result_write( fs::write(path, what) } -#[then(regex = r#"^the file `([^`\s]+)` should contain "(\S+)"$"#)] +#[then(expr = "the file {string} should contain {string}")] fn test_return_result_read( w: &mut MyWorld, filename: String, what: String, ) -> io::Result<()> { let mut path = w.dir.path().to_path_buf(); - path.push(filename); + path.push(filename.trim_matches('\'')); - assert_eq!(what, fs::read_to_string(path)?); + assert_eq!(what.trim_matches('"'), fs::read_to_string(path)?); Ok(()) } diff --git a/codegen/tests/features/example.feature b/codegen/tests/features/example.feature index 9ee37183..b1c9fcc5 100644 --- a/codegen/tests/features/example.feature +++ b/codegen/tests/features/example.feature @@ -16,9 +16,9 @@ Feature: Example feature Given foo is sync 0 Scenario: Steps returning result - When I write "abc" to `myfile.txt` - Then the file `myfile.txt` should contain "abc" + When I write "abc" to 'myfile.txt' + Then the file 'myfile.txt' should contain "abc" Scenario: Steps returning result and failing - When I write "abc" to `myfile.txt` - Then the file `not-here.txt` should contain "abc" + When I write "abc" to 'myfile.txt' + Then the file 'not-here.txt' should contain "abc" From d4c8bf9e679ececb10ff4db061d4d999fcd7fb44 Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 08:18:44 +0300 Subject: [PATCH 2/9] Switch to crates.io release --- codegen/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 645f5cf6..5cc6db09 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -21,8 +21,7 @@ exclude = ["/tests/"] proc-macro = true [dependencies] -# TODO: switch to crates.io -cucumber-expressions = { git = "https://github.com/ilslv/cucumber-expressions-1.git" } +cucumber-expressions = { version = "0.1", features = ["into-regex"] } inflections = "1.1" itertools = "0.10" proc-macro2 = "1.0.28" From a97c27e00785da7b105b62cb2dc9b67ddfbcdce2 Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 08:29:21 +0300 Subject: [PATCH 3/9] Add README example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8174e04..12ff34a5 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ impl cucumber::World for World { } } -#[given(regex = r"^(\S+) is hungry$")] +#[given(expr = r"{word} is hungry")] async fn someone_is_hungry(w: &mut World, user: String) { sleep(Duration::from_secs(2)).await; @@ -69,7 +69,7 @@ async fn eat_cucumbers(w: &mut World, count: usize) { assert!(w.capacity < 4, "{} exploded!", w.user.as_ref().unwrap()); } -#[then(regex = r"^(?:he|she|they) (?:is|are) full$")] +#[then(expr = r"he/she/they is/are full")] async fn is_full(w: &mut World) { sleep(Duration::from_secs(2)).await; From ef5e6295082baa5250b96f24b7206d776f9eee8b Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 08:29:31 +0300 Subject: [PATCH 4/9] Add README example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12ff34a5..c090f678 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ impl cucumber::World for World { } } -#[given(expr = r"{word} is hungry")] +#[given(expr = "{word} is hungry")] async fn someone_is_hungry(w: &mut World, user: String) { sleep(Duration::from_secs(2)).await; @@ -69,7 +69,7 @@ async fn eat_cucumbers(w: &mut World, count: usize) { assert!(w.capacity < 4, "{} exploded!", w.user.as_ref().unwrap()); } -#[then(expr = r"he/she/they is/are full")] +#[then(expr = "he/she/they is/are full")] async fn is_full(w: &mut World) { sleep(Duration::from_secs(2)).await; From 18c1a45bf52f353abe1693621c0a5674b3dd0f39 Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 08:42:01 +0300 Subject: [PATCH 5/9] CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f729275..c6e6820f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - `writer::Normalized` trait required for `Writer`s in `Cucumber` running methods. ([#162]) - `writer::NonTransforming` trait required for `writer::Repeat`. ([#162]) - `writer::Summarizable` trait required for `writer::Summarize`. ([#162]) +- Support for [Cucumber Expressions]. ([#157]) ### Fixed @@ -36,12 +37,14 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [#147]: /../../pull/147 [#151]: /../../pull/151 +[#157]: /../../pull/157 [#159]: /../../pull/159 [#160]: /../../pull/160 [#162]: /../../pull/162 [#163]: /../../pull/163 [0110-1]: https://llg.cubic.org/docs/junit [0110-2]: https://github.com/cucumber/cucumber-json-schema +[Cucumber Expressions]: https://docs.rs/cucumber-expressions From e80759a0d9a50f78a1fb502cd7a7ea9143a78c9f Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 09:29:53 +0300 Subject: [PATCH 6/9] Merge Feature and Rule tags on filtering --- book/src/Features.md | 4 +++- src/cli.rs | 4 +++- src/cucumber.rs | 19 +++++++++++++++---- src/tag.rs | 17 ++++++++++++----- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/book/src/Features.md b/book/src/Features.md index 63bfde16..42ddab0d 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -411,7 +411,9 @@ OPTIONS: -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] + -t, --tags Tag expression to filter scenarios by. + Note: Tags from Feature, Rule and Scenario are merged together on filtering, + so be careful about conflicting tags on different levels. ``` Example with [tag expressions](https://cucumber.io/docs/cucumber/api#tag-expressions) for filtering `Scenario`s: diff --git a/src/cli.rs b/src/cli.rs index 1ff0d56a..3f75e85a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -113,11 +113,13 @@ where pub re_filter: Option, /// Tag expression to filter scenarios by. + /// + /// Note: Tags from Feature, Rule and Scenario are merged together on + /// filtering, so be careful about conflicting tags on different levels. #[structopt( short = "t", long = "tags", name = "tagexpr", - visible_alias = "scenario-tags", conflicts_with = "regex" )] pub tags_filter: Option, diff --git a/src/cucumber.rs b/src/cucumber.rs index 64b52e31..9117b435 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -818,11 +818,22 @@ where s: &gherkin::Scenario| { re_filter.as_ref().map_or_else( || { - tags_filter - .as_ref() - .map_or_else(|| filter(f, r, s), |f| f.eval(&s.tags)) + tags_filter.as_ref().map_or_else( + || filter(f, r, s), + |tags| { + tags.eval( + f.tags + .iter() + .chain( + r.into_iter() + .flat_map(|r| r.tags.iter()), + ) + .chain(s.tags.iter()), + ) + }, + ) }, - |f| f.is_match(&s.name), + |re| re.is_match(&s.name), ) }; diff --git a/src/tag.rs b/src/tag.rs index 27ec75c4..4b2f25d3 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -18,17 +18,24 @@ use sealed::sealed; pub trait Ext { /// Evaluates this [`TagOperation`] for the given `tags`. #[must_use] - fn eval(&self, tags: &[String]) -> bool; + fn eval(&self, tags: I) -> bool + where + S: AsRef, + I: Iterator + Clone; } #[sealed] impl Ext for TagOperation { - fn eval(&self, tags: &[String]) -> bool { + fn eval(&self, mut tags: I) -> bool + where + S: AsRef, + I: Iterator + Clone, + { match self { - Self::And(l, r) => l.eval(tags) & r.eval(tags), - Self::Or(l, r) => l.eval(tags) | r.eval(tags), + Self::And(l, r) => l.eval(tags.clone()) & r.eval(tags), + Self::Or(l, r) => l.eval(tags.clone()) | r.eval(tags), Self::Not(t) => !t.eval(tags), - Self::Tag(t) => tags.iter().any(|tag| tag == t), + Self::Tag(t) => tags.any(|tag| tag.as_ref() == t), } } } From 63ad173c9e903fb1121d99578ef443bb16d2b98b Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 09:38:26 +0300 Subject: [PATCH 7/9] CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e6820f..fe3f28b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - Moved `World` type parameter of `WriterExt` trait to methods. ([#160]) - Renamed `Normalized` and `Summarized` `Writer`s to `Normalize` and `Summarize`. ([#162]) - Removed `writer::Basic` `Default` impl and change `writer::Basic::new()` return type to `writer::Normalize`. ([#162]) +- Merge tags from `Feature` and `Rule` with `Scenario` on filtering with `--tags` CLI option. ([#166]) ### Added @@ -42,6 +43,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [#160]: /../../pull/160 [#162]: /../../pull/162 [#163]: /../../pull/163 +[#166]: /../../pull/166 [0110-1]: https://llg.cubic.org/docs/junit [0110-2]: https://github.com/cucumber/cucumber-json-schema [Cucumber Expressions]: https://docs.rs/cucumber-expressions From 8a2feb21ff7e0304f644e6ba5c37227d9422a815 Mon Sep 17 00:00:00 2001 From: ilslv Date: Tue, 23 Nov 2021 15:26:23 +0300 Subject: [PATCH 8/9] Add `FromStr` example with `cucumber-expressions` --- book/src/Features.md | 119 ++++++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 41 deletions(-) diff --git a/book/src/Features.md b/book/src/Features.md index 42ddab0d..2a0ca120 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -194,38 +194,42 @@ Feature: Animal feature ```rust # use std::{convert::Infallible, str::FromStr, time::Duration}; -# +# # use async_trait::async_trait; # use cucumber::{given, then, when, World, WorldInit}; # use tokio::time::sleep; -# -# #[derive(Debug)] -# struct Cat { -# pub hungry: bool, -# } -# -# impl Cat { -# fn feed(&mut self) { -# self.hungry = false; -# } -# } -# -# #[derive(Debug, WorldInit)] -# pub struct AnimalWorld { -# cat: Cat, -# } -# -# #[async_trait(?Send)] -# impl World for AnimalWorld { -# type Error = Infallible; -# -# async fn new() -> Result { -# Ok(Self { -# cat: Cat { hungry: false }, -# }) -# } -# } -# +# +#[derive(Debug)] +struct AnimalState { + pub hungry: bool +} + +impl AnimalState { + fn feed(&mut self) { + self.hungry = false; + } +} + +#[derive(Debug, WorldInit)] +pub struct AnimalWorld { + cat: AnimalState, + dog: AnimalState, + ferris: AnimalState, +} + +#[async_trait(?Send)] +impl World for AnimalWorld { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self { + cat: AnimalState { hungry: false }, + dog: AnimalState { hungry: false }, + ferris: AnimalState { hungry: false }, + }) + } +} + enum State { Hungry, Satiated, @@ -243,33 +247,66 @@ impl FromStr for State { } } +enum Animal { + Cat, + Dog, + Ferris, +} + +impl FromStr for Animal { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "cat" => Ok(Self::Cat), + "dog" => Ok(Self::Dog), + "🦀" => Ok(Self::Ferris), + _ => Err("expected 'cat', 'dog' or '🦀'"), + } + } +} + #[given(regex = r"^a (\S+) (\S+)$")] -async fn hungry_cat(world: &mut AnimalWorld, state: State) { +async fn hungry_cat(world: &mut AnimalWorld, state: State, animal: Animal) { sleep(Duration::from_secs(2)).await; - match state { - State::Hungry => world.cat.hungry = true, - State::Satiated => world.cat.hungry = false, - } + let hunger = match state { + State::Hungry => true, + State::Satiated => false, + }; + + match animal { + Animal::Cat => world.cat.hungry = hunger, + Animal::Dog => world.dog.hungry = hunger, + Animal::Ferris => world.ferris.hungry = hunger, + }; } -#[when(regex = r"^I feed the (?:\S+) (\d+) times?$")] -async fn feed_cat(world: &mut AnimalWorld, times: usize) { +#[when(regex = r"^I feed the (\S+) (\d+) times?$")] +async fn feed_cat(world: &mut AnimalWorld, animal: Animal, times: usize) { sleep(Duration::from_secs(2)).await; for _ in 0..times { - world.cat.feed(); + match animal { + Animal::Cat => world.cat.feed(), + Animal::Dog => world.dog.feed(), + Animal::Ferris => world.ferris.feed(), + }; } } #[then(expr = "the {word} is not hungry")] -async fn cat_is_fed(world: &mut AnimalWorld) { +async fn cat_is_fed(world: &mut AnimalWorld, animal: Animal) { sleep(Duration::from_secs(2)).await; - assert!(!world.cat.hungry); + match animal { + Animal::Cat => assert!(!world.cat.hungry), + Animal::Dog => assert!(!world.dog.hungry), + Animal::Ferris => assert!(!world.ferris.hungry), + }; } -# -# #[tokio::main] +# +# # [tokio::main] # async fn main() { # AnimalWorld::run("/tests/features/book/features/scenario_outline_fromstr.feature").await; # } From f794d6ef5fa5c52bf0e6e780535f1abbac869ebd Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 26 Nov 2021 17:50:26 +0100 Subject: [PATCH 9/9] Corrections --- CHANGELOG.md | 2 +- book/src/Features.md | 2 +- src/cucumber.rs | 2 ++ src/tag.rs | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815cce74..669c9025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - Moved `World` type parameter of `WriterExt` trait to methods. ([#160]) - Renamed `Normalized` and `Summarized` `Writer`s to `Normalize` and `Summarize`. ([#162]) - Removed `writer::Basic` `Default` impl and change `writer::Basic::new()` return type to `writer::Normalize`. ([#162]) -- Merge tags from `Feature` and `Rule` with `Scenario` on filtering with `--tags` CLI option. ([#166]) ### Added @@ -31,6 +30,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th - `writer::NonTransforming` trait required for `writer::Repeat`. ([#162]) - `writer::Summarizable` trait required for `writer::Summarize`. ([#162]) - Support for [Cucumber Expressions] via `#[given(expr = ...)]`, `#[when(expr = ...)]` and `#[then(expr = ...)]` syntax. ([#157]) +- Merging tags from `Feature` and `Rule` with `Scenario` when filtering with `--tags` CLI option. ([#166]) ### Fixed diff --git a/book/src/Features.md b/book/src/Features.md index 98863f3d..e92d4151 100644 --- a/book/src/Features.md +++ b/book/src/Features.md @@ -430,7 +430,7 @@ cargo test --test -- --help Default output is: ``` -cucumber 0.10.0 +cucumber Run the tests, pet a dog! USAGE: diff --git a/src/cucumber.rs b/src/cucumber.rs index 9117b435..7105291f 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -821,6 +821,8 @@ where tags_filter.as_ref().map_or_else( || filter(f, r, s), |tags| { + // The order `Feature` -> `Rule` -> `Scenario` + // matters here. tags.eval( f.tags .iter() diff --git a/src/tag.rs b/src/tag.rs index 4b2f25d3..54509ac1 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -21,21 +21,21 @@ pub trait Ext { fn eval(&self, tags: I) -> bool where S: AsRef, - I: Iterator + Clone; + I: IntoIterator + Clone; } #[sealed] impl Ext for TagOperation { - fn eval(&self, mut tags: I) -> bool + fn eval(&self, tags: I) -> bool where S: AsRef, - I: Iterator + Clone, + I: IntoIterator + Clone, { match self { Self::And(l, r) => l.eval(tags.clone()) & r.eval(tags), Self::Or(l, r) => l.eval(tags.clone()) | r.eval(tags), Self::Not(t) => !t.eval(tags), - Self::Tag(t) => tags.any(|tag| tag.as_ref() == t), + Self::Tag(t) => tags.into_iter().any(|tag| tag.as_ref() == t), } } }