Skip to content

Commit

Permalink
Basic support of Cucumber Expressions without custom parameters (#157,
Browse files Browse the repository at this point in the history
…#124)

- add `expr` argument to `#[given]`, `#[when]` and `#[then]` attributes
  • Loading branch information
ilslv committed Nov 23, 2021
1 parent f97d0a3 commit 7bc13da
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 75 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ 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] via `#[given(expr = ...)]`, `#[when(expr = ...)]` and `#[then(expr = ...)]` syntax. ([#157])

### Fixed

- Template regex in `Scenario Outline` expansion from `<(\S+)>` to `<([^>\s]+)>`. ([#163])

[#147]: /../../pull/147
[#151]: /../../pull/151
[#157]: /../../pull/157
[#159]: /../../pull/159
[#160]: /../../pull/160
[#162]: /../../pull/162
Expand Down Expand Up @@ -263,4 +265,5 @@ All user visible changes to `cucumber` crate will be documented in this file. Th

[`gherkin_rust`]: https://docs.rs/gherkin_rust

[Cucumber Expressions]: https://cucumber.github.io/cucumber-expressions
[Semantic Versioning 2.0.0]: https://semver.org
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl cucumber::World for World {
}
}

#[given(regex = r"^(\S+) is hungry$")]
#[given(expr = "{word} is hungry")]
async fn someone_is_hungry(w: &mut World, user: String) {
sleep(Duration::from_secs(2)).await;

Expand All @@ -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 = "he/she/they is/are full")]
async fn is_full(w: &mut World) {
sleep(Duration::from_secs(2)).await;

Expand Down
121 changes: 79 additions & 42 deletions book/src/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ async fn cat_is_fed(world: &mut AnimalWorld) {
<script id="asciicast-o1s4mSMYkkVBy4WAsG8lhYtT8" src="https://asciinema.org/a/o1s4mSMYkkVBy4WAsG8lhYtT8.js" async data-autoplay="true" data-rows="18"></script>


### Combining `regex` and `FromStr`
### Combining `regex`/`cucumber-expressions` and `FromStr`

At parsing stage, `<templates>` 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).

Expand All @@ -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<Self, Infallible> {
# 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<Self, Infallible> {
Ok(Self {
cat: AnimalState { hungry: false },
dog: AnimalState { hungry: false },
ferris: AnimalState { hungry: false },
})
}
}

enum State {
Hungry,
Satiated,
Expand All @@ -243,32 +247,65 @@ impl FromStr for State {
}
}

enum Animal {
Cat,
Dog,
Ferris,
}

impl FromStr for Animal {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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(regex = r"^the (\S+) is not hungry$")]
async fn cat_is_fed(world: &mut AnimalWorld) {
#[then(expr = "the {word} is not hungry")]
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]
# async fn main() {
# AnimalWorld::run("/tests/features/book/features/scenario_outline_fromstr.feature").await;
Expand Down
59 changes: 59 additions & 0 deletions book/src/Getting_Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Alternatively, you may use [Cucumber Expressions] for the same purpose (less powerful, but much more readable):
```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<Self, Infallible> {
# 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.

<details>
Expand Down Expand Up @@ -540,4 +598,5 @@ Feature: Animal feature


[Cucumber]: https://cucumber.io
[Cucumber Expressions]: https://cucumber.github.io/cucumber-expressions
[Gherkin]: https://cucumber.io/docs/gherkin/reference
2 changes: 1 addition & 1 deletion book/src/Test_Modules_Organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.



Expand Down
3 changes: 3 additions & 0 deletions codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ All user visible changes to `cucumber-codegen` crate will be documented in this
### Added

- Unwrapping `Result`s returned by step functions. ([#151])
- `expr = ...` argument to `#[given(...)]`, `#[when(...)]` and `#[then(...)]` attributes allowing [Cucumber Expressions]. ([#157])

[#151]: /../../pull/151
[#157]: /../../pull/157



Expand Down Expand Up @@ -76,4 +78,5 @@ See `cucumber` crate [changelog](https://github.com/cucumber-rs/cucumber/blob/v0



[Cucumber Expressions]: https://cucumber.github.io/cucumber-expressions
[Semantic Versioning 2.0.0]: https://semver.org
1 change: 1 addition & 0 deletions codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exclude = ["/tests/"]
proc-macro = true

[dependencies]
cucumber-expressions = { version = "0.1", features = ["into-regex"] }
inflections = "1.1"
itertools = "0.10"
proc-macro2 = "1.0.28"
Expand Down
Loading

0 comments on commit 7bc13da

Please sign in to comment.