Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Cucumber Expressions (#124) #157

Merged
merged 8 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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