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

Restore before and after hooks (#141) #142

Merged
merged 10 commits into from
Oct 18, 2021
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 `&mut World` as a 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
76 changes: 76 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.

A `Background` allows you to add some context to the scenarios that follow 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,79 @@ In case most of your `.feature` files aren't written in English and you want to



## Scenario hooks

### Before hook

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

```rust
# use std::{convert::Infallible, time::Duration};
#
# use async_trait::async_trait;
# use cucumber::WorldInit;
# use futures::FutureExt as _;
# use tokio::time::sleep;
#
# #[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| {
sleep(Duration::from_millis(10)).boxed_local()
})
.run_and_exit("tests/features/book");
# }
```

> #### Think twice before you use `Before`
> Whatever happens in a `Before` hook is invisible to people who only read the features. 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` hooks run after the last step of each scenario, even when the step result is `failed` or `skipped`.

```rust
# use std::{convert::Infallible, time::Duration};
#
# use async_trait::async_trait;
# use cucumber::WorldInit;
# use futures::FutureExt as _;
# use tokio::time::sleep;
#
# #[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| {
sleep(Duration::from_millis(10)).boxed_local()
})
.run_and_exit("tests/features/book");
# }
```




[Cucumber]: https://cucumber.io
[Gherkin]: https://cucumber.io/docs/gherkin
104 changes: 86 additions & 18 deletions src/cucumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::{
};

use clap::Clap 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 hook, executed on every [`Scenario`] before any [`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 hook, executed on every [`Scenario`] after all [`Step`]s even after
/// [`Skipped`] of [`Failed`] [`Step`]s.
///
/// Last `World` argument is supplied to the function, in case it
/// was initialized before by [`before`] hook or any non-failed [`Step`].
/// In case last [`Scenario`]'s [`Step`] failed, we want to return event
/// with 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 @@ -1029,12 +1094,15 @@ where
if writer.execution_has_failed() {
let failed_steps = writer.failed_steps();
let parsing_errors = writer.parsing_errors();
let hook_errors = writer.hook_errors();
panic!(
"{} step{} failed, {} parsing error{}",
"{} step{} failed, {} parsing error{}, {} hook error{}",
failed_steps,
(failed_steps != 1).then(|| "s").unwrap_or_default(),
parsing_errors,
(parsing_errors != 1).then(|| "s").unwrap_or_default(),
hook_errors,
(hook_errors != 1).then(|| "s").unwrap_or_default(),
);
}
}
Expand Down
91 changes: 90 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
//! [`Runner`]: crate::Runner
//! [Cucumber]: https://cucumber.io

use std::{any::Any, sync::Arc};
use std::{any::Any, fmt, sync::Arc};

/// Alias for a [`catch_unwind()`] error.
///
Expand Down Expand Up @@ -224,6 +224,63 @@ impl<World> Clone for Step<World> {
}
}

/// Type of the hook, executed before or after all [`Scenario`]'s [`Step`]s.
///
/// [`Scenario`]: gherkin::Scenario
/// [`Step`]: gherkin::Step
#[derive(Clone, Copy, Debug)]
pub enum HookTy {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In type names it's better not to use shortcuts. They're good for local variables only.

/// Hook, executed on every [`Scenario`] before any [`Step`]s.
///
/// [`Scenario`]: gherkin::Scenario
/// [`Step`]: gherkin::Step
Before,

/// Hook, executed on every [`Scenario`] after all [`Step`]s.
///
/// [`Scenario`]: gherkin::Scenario
/// [`Step`]: gherkin::Step
After,
}

/// [`Before`] or [`After`] hook event.
///
/// [`After`]: HookTy::After
/// [`Before`]: HookTy::Before
#[derive(Debug)]
pub enum Hook<World> {
/// Hook execution being started.
Started,

/// Hook passed.
Passed,

/// Hook failed.
Failed(Option<Arc<World>>, Info),
}

impl fmt::Display for HookTy {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, do not mix declarations and impls of different types. It's very misleading.

fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
HookTy::Before => "Before",
HookTy::After => "After",
};
write!(f, "{}", s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just piggyback to Debug impl here.

}
}

// Manual implementation is required to omit the redundant `World: Clone` trait
// bound imposed by `#[derive(Clone)]`.
impl<World> Clone for Hook<World> {
fn clone(&self) -> Self {
match self {
Hook::Started => Hook::Started,
Hook::Passed => Hook::Passed,
Hook::Failed(w, i) => Hook::Failed(w.clone(), i.clone()),
}
}
}

/// Event specific to a particular [Scenario].
///
/// [Scenario]: https://cucumber.io/docs/gherkin/reference/#example
Expand All @@ -234,6 +291,9 @@ pub enum Scenario<World> {
/// [`Scenario`]: gherkin::Scenario
Started,

/// [`Hook`] event.
Hook(HookTy, Hook<World>),

/// [`Background`] [`Step`] event.
///
/// [`Background`]: gherkin::Background
Expand All @@ -254,6 +314,7 @@ impl<World> Clone for Scenario<World> {
fn clone(&self) -> Self {
match self {
Self::Started => Self::Started,
Self::Hook(ty, ev) => Self::Hook(*ty, ev.clone()),
Self::Background(bg, ev) => {
Self::Background(bg.clone(), ev.clone())
}
Expand All @@ -264,6 +325,34 @@ impl<World> Clone for Scenario<World> {
}

impl<World> Scenario<World> {
/// Constructs an event of a [`Scenario`] hook being started.
///
/// [`Scenario`]: gherkin::Scenario
#[must_use]
pub fn hook_started(which: HookTy) -> Self {
Self::Hook(which, Hook::Started)
}

/// Constructs an event of a passed [`Scenario`] hook.
///
/// [`Scenario`]: gherkin::Scenario
#[must_use]
pub fn hook_passed(which: HookTy) -> Self {
Self::Hook(which, Hook::Passed)
}

/// Constructs an event of a failed [`Scenario`] hook.
///
/// [`Scenario`]: gherkin::Scenario
#[must_use]
pub fn hook_failed(
which: HookTy,
world: Option<Arc<World>>,
info: Info,
) -> Self {
Self::Hook(which, Hook::Failed(world, info))
}

/// Constructs an event of a [`Step`] being started.
///
/// [`Step`]: gherkin::Step
Expand Down
Loading