Skip to content

Commit

Permalink
Restore before and after scenario hooks (#142, #141)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilslv authored Oct 18, 2021
1 parent d494dbc commit b82659e
Show file tree
Hide file tree
Showing 13 changed files with 849 additions and 106 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
- 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])
- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) were removed, but are planned to be re-implemented with some changes in `0.11` release. ([#128])
- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept optional `&mut World` as their last parameter. ([#142])

### Added

Expand All @@ -32,6 +32,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
[#128]: /../../pull/128
[#136]: /../../pull/136
[#137]: /../../pull/137
[#142]: /../../pull/142



Expand Down
78 changes: 78 additions & 0 deletions book/src/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Occasionally you’ll find yourself repeating the same `Given` steps in all the

Since it's repeated in every scenario, this is an indication that those steps are not essential to describe the scenarios, so they are _incidental details_. You can literally move such `Given` steps to background, by grouping them under a `Background` section.

`Background` allows you to add some context to the `Scenario`s following it. It can contain one or more steps, which are run before each scenario (but after any [`Before` hooks](#before-hook)).

```gherkin
Feature: Animal feature
Expand Down Expand Up @@ -304,5 +306,81 @@ In case most of your `.feature` files aren't written in English and you want to



## Scenario hooks


### `Before` hook

`Before` hook runs before the first step of each scenario, even before [`Background` ones](#background-keyword).

```rust
# use std::{convert::Infallible, time::Duration};
#
# use async_trait::async_trait;
# use cucumber::WorldInit;
# use futures::FutureExt as _;
# use tokio::time;
#
# #[derive(Debug, WorldInit)]
# struct World;
#
# #[async_trait(?Send)]
# impl cucumber::World for World {
# type Error = Infallible;
#
# async fn new() -> Result<Self, Self::Error> {
# Ok(World)
# }
# }
#
# fn main() {
World::cucumber()
.before(|_feature, _rule, _scenario, _world| {
time::sleep(Duration::from_millis(10)).boxed_local()
})
.run_and_exit("tests/features/book");
# }
```

> ⚠️ __Think twice before using `Before` hook!__
> Whatever happens in a `Before` hook is invisible to people reading `.feature`s. You should consider using a [`Background`](#background-keyword) as a more explicit alternative, especially if the setup should be readable by non-technical people. Only use a `Before` hook for low-level logic such as starting a browser or deleting data from a database.

### `After` hook

`After` hook runs after the last step of each `Scenario`, even when that step fails or is skipped.

```rust
# use std::{convert::Infallible, time::Duration};
#
# use async_trait::async_trait;
# use cucumber::WorldInit;
# use futures::FutureExt as _;
# use tokio::time;
#
# #[derive(Debug, WorldInit)]
# struct World;
#
# #[async_trait(?Send)]
# impl cucumber::World for World {
# type Error = Infallible;
#
# async fn new() -> Result<Self, Self::Error> {
# Ok(World)
# }
# }
#
# fn main() {
World::cucumber()
.after(|_feature, _rule, _scenario, _world| {
time::sleep(Duration::from_millis(10)).boxed_local()
})
.run_and_exit("tests/features/book");
# }
```




[Cucumber]: https://cucumber.io
[Gherkin]: https://cucumber.io/docs/gherkin
2 changes: 1 addition & 1 deletion book/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ publish = false

[dependencies]
async-trait = "0.1"
cucumber = { path = "../.." }
cucumber = { version = "0.10", path = "../.." }
futures = "0.3"
skeptic = "0.13"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
Expand Down
134 changes: 110 additions & 24 deletions src/cucumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::{
};

use clap::Parser as _;
use futures::StreamExt as _;
use futures::{future::LocalBoxFuture, StreamExt as _};
use regex::Regex;

use crate::{
Expand Down Expand Up @@ -774,22 +774,9 @@ where
I: AsRef<Path>,
{
fn default() -> Self {
let which: runner::basic::WhichScenarioFn = |_, _, scenario| {
scenario
.tags
.iter()
.any(|tag| tag == "serial")
.then(|| ScenarioType::Serial)
.unwrap_or(ScenarioType::Concurrent)
};

Cucumber::custom()
.with_parser(parser::Basic::new())
.with_runner(
runner::Basic::custom()
.which_scenario(which)
.max_concurrent_scenarios(64),
)
.with_runner(runner::Basic::default())
.with_writer(writer::Basic::new().normalized().summarized())
}
}
Expand Down Expand Up @@ -840,7 +827,7 @@ impl<W, I, R, Wr> Cucumber<W, parser::Basic, I, R, Wr> {
}
}

impl<W, I, P, Wr, F> Cucumber<W, P, I, runner::Basic<W, F>, Wr> {
impl<W, I, P, Wr, F, B, A> Cucumber<W, P, I, runner::Basic<W, F, B, A>, Wr> {
/// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will
/// be limited.
///
Expand All @@ -864,7 +851,7 @@ impl<W, I, P, Wr, F> Cucumber<W, P, I, runner::Basic<W, F>, Wr> {
pub fn which_scenario<Which>(
self,
func: Which,
) -> Cucumber<W, P, I, runner::Basic<W, Which>, Wr>
) -> Cucumber<W, P, I, runner::Basic<W, Which, B, A>, Wr>
where
Which: Fn(
&gherkin::Feature,
Expand All @@ -888,6 +875,84 @@ impl<W, I, P, Wr, F> Cucumber<W, P, I, runner::Basic<W, F>, Wr> {
}
}

/// Sets a hook, executed on each [`Scenario`] before running all its
/// [`Step`]s, including [`Background`] ones.
///
/// [`Background`]: gherkin::Background
/// [`Scenario`]: gherkin::Scenario
/// [`Step`]: gherkin::Step
#[must_use]
pub fn before<Before>(
self,
func: Before,
) -> Cucumber<W, P, I, runner::Basic<W, F, Before, A>, Wr>
where
Before: for<'a> Fn(
&'a gherkin::Feature,
Option<&'a gherkin::Rule>,
&'a gherkin::Scenario,
&'a mut W,
) -> LocalBoxFuture<'a, ()>,
{
let Self {
parser,
runner,
writer,
..
} = self;
Cucumber {
parser,
runner: runner.before(func),
writer,
_world: PhantomData,
_parser_input: PhantomData,
}
}

/// Sets a hook, executed on each [`Scenario`] after running all its
/// [`Step`]s, even after [`Skipped`] of [`Failed`] [`Step`]s.
///
/// Last `World` argument is supplied to the function, in case it was
/// initialized before by running [`before`] hook or any non-failed
/// [`Step`]. In case the last [`Scenario`]'s [`Step`] failed, we want to
/// return event with an exact `World` state. Also, we don't want to impose
/// additional [`Clone`] bounds on `World`, so the only option left is to
/// pass [`None`] to the function.
///
///
/// [`before`]: Self::before()
/// [`Failed`]: event::Step::Failed
/// [`Scenario`]: gherkin::Scenario
/// [`Skipped`]: event::Step::Skipped
/// [`Step`]: gherkin::Step
#[must_use]
pub fn after<After>(
self,
func: After,
) -> Cucumber<W, P, I, runner::Basic<W, F, B, After>, Wr>
where
After: for<'a> Fn(
&'a gherkin::Feature,
Option<&'a gherkin::Rule>,
&'a gherkin::Scenario,
Option<&'a mut W>,
) -> LocalBoxFuture<'a, ()>,
{
let Self {
parser,
runner,
writer,
..
} = self;
Cucumber {
parser,
runner: runner.after(func),
writer,
_world: PhantomData,
_parser_input: PhantomData,
}
}

/// Replaces [`Collection`] of [`Step`]s.
///
/// [`Collection`]: step::Collection
Expand Down Expand Up @@ -1027,15 +1092,36 @@ where
{
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();
panic!(
"{} step{} failed, {} parsing error{}",
failed_steps,
(failed_steps != 1).then(|| "s").unwrap_or_default(),
parsing_errors,
(parsing_errors != 1).then(|| "s").unwrap_or_default(),
);
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(", "));
}
}
}
Loading

0 comments on commit b82659e

Please sign in to comment.