Skip to content

Commit b82659e

Browse files
authored
Restore before and after scenario hooks (#142, #141)
1 parent d494dbc commit b82659e

File tree

13 files changed

+849
-106
lines changed

13 files changed

+849
-106
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
2222
- Made test callbacks first argument `&mut World` instead of `World`. ([#128])
2323
- Made `#[step]` argument of step functions `Step` instead of `StepContext` again, while test callbacks still receive `StepContext` as a second parameter. ([#128])
2424
- Deprecated `--nocapture` and `--debug` CLI options to be completely redesigned in `0.11` release. ([#137])
25-
- [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])
25+
- [Hooks](https://cucumber.io/docs/cucumber/api/#hooks) now accept optional `&mut World` as their last parameter. ([#142])
2626

2727
### Added
2828

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

3637

3738

book/src/Features.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Occasionally you’ll find yourself repeating the same `Given` steps in all the
4141

4242
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.
4343

44+
`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)).
45+
4446
```gherkin
4547
Feature: Animal feature
4648
@@ -304,5 +306,81 @@ In case most of your `.feature` files aren't written in English and you want to
304306

305307

306308

309+
## Scenario hooks
310+
311+
312+
### `Before` hook
313+
314+
`Before` hook runs before the first step of each scenario, even before [`Background` ones](#background-keyword).
315+
316+
```rust
317+
# use std::{convert::Infallible, time::Duration};
318+
#
319+
# use async_trait::async_trait;
320+
# use cucumber::WorldInit;
321+
# use futures::FutureExt as _;
322+
# use tokio::time;
323+
#
324+
# #[derive(Debug, WorldInit)]
325+
# struct World;
326+
#
327+
# #[async_trait(?Send)]
328+
# impl cucumber::World for World {
329+
# type Error = Infallible;
330+
#
331+
# async fn new() -> Result<Self, Self::Error> {
332+
# Ok(World)
333+
# }
334+
# }
335+
#
336+
# fn main() {
337+
World::cucumber()
338+
.before(|_feature, _rule, _scenario, _world| {
339+
time::sleep(Duration::from_millis(10)).boxed_local()
340+
})
341+
.run_and_exit("tests/features/book");
342+
# }
343+
```
344+
345+
> ⚠️ __Think twice before using `Before` hook!__
346+
> 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.
347+
348+
349+
### `After` hook
350+
351+
`After` hook runs after the last step of each `Scenario`, even when that step fails or is skipped.
352+
353+
```rust
354+
# use std::{convert::Infallible, time::Duration};
355+
#
356+
# use async_trait::async_trait;
357+
# use cucumber::WorldInit;
358+
# use futures::FutureExt as _;
359+
# use tokio::time;
360+
#
361+
# #[derive(Debug, WorldInit)]
362+
# struct World;
363+
#
364+
# #[async_trait(?Send)]
365+
# impl cucumber::World for World {
366+
# type Error = Infallible;
367+
#
368+
# async fn new() -> Result<Self, Self::Error> {
369+
# Ok(World)
370+
# }
371+
# }
372+
#
373+
# fn main() {
374+
World::cucumber()
375+
.after(|_feature, _rule, _scenario, _world| {
376+
time::sleep(Duration::from_millis(10)).boxed_local()
377+
})
378+
.run_and_exit("tests/features/book");
379+
# }
380+
```
381+
382+
383+
384+
307385
[Cucumber]: https://cucumber.io
308386
[Gherkin]: https://cucumber.io/docs/gherkin

book/tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ publish = false
1212

1313
[dependencies]
1414
async-trait = "0.1"
15-
cucumber = { path = "../.." }
15+
cucumber = { version = "0.10", path = "../.." }
1616
futures = "0.3"
1717
skeptic = "0.13"
1818
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }

src/cucumber.rs

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::{
2121
};
2222

2323
use clap::Parser as _;
24-
use futures::StreamExt as _;
24+
use futures::{future::LocalBoxFuture, StreamExt as _};
2525
use regex::Regex;
2626

2727
use crate::{
@@ -774,22 +774,9 @@ where
774774
I: AsRef<Path>,
775775
{
776776
fn default() -> Self {
777-
let which: runner::basic::WhichScenarioFn = |_, _, scenario| {
778-
scenario
779-
.tags
780-
.iter()
781-
.any(|tag| tag == "serial")
782-
.then(|| ScenarioType::Serial)
783-
.unwrap_or(ScenarioType::Concurrent)
784-
};
785-
786777
Cucumber::custom()
787778
.with_parser(parser::Basic::new())
788-
.with_runner(
789-
runner::Basic::custom()
790-
.which_scenario(which)
791-
.max_concurrent_scenarios(64),
792-
)
779+
.with_runner(runner::Basic::default())
793780
.with_writer(writer::Basic::new().normalized().summarized())
794781
}
795782
}
@@ -840,7 +827,7 @@ impl<W, I, R, Wr> Cucumber<W, parser::Basic, I, R, Wr> {
840827
}
841828
}
842829

843-
impl<W, I, P, Wr, F> Cucumber<W, P, I, runner::Basic<W, F>, Wr> {
830+
impl<W, I, P, Wr, F, B, A> Cucumber<W, P, I, runner::Basic<W, F, B, A>, Wr> {
844831
/// If `max` is [`Some`] number of concurrently executed [`Scenario`]s will
845832
/// be limited.
846833
///
@@ -864,7 +851,7 @@ impl<W, I, P, Wr, F> Cucumber<W, P, I, runner::Basic<W, F>, Wr> {
864851
pub fn which_scenario<Which>(
865852
self,
866853
func: Which,
867-
) -> Cucumber<W, P, I, runner::Basic<W, Which>, Wr>
854+
) -> Cucumber<W, P, I, runner::Basic<W, Which, B, A>, Wr>
868855
where
869856
Which: Fn(
870857
&gherkin::Feature,
@@ -888,6 +875,84 @@ impl<W, I, P, Wr, F> Cucumber<W, P, I, runner::Basic<W, F>, Wr> {
888875
}
889876
}
890877

878+
/// Sets a hook, executed on each [`Scenario`] before running all its
879+
/// [`Step`]s, including [`Background`] ones.
880+
///
881+
/// [`Background`]: gherkin::Background
882+
/// [`Scenario`]: gherkin::Scenario
883+
/// [`Step`]: gherkin::Step
884+
#[must_use]
885+
pub fn before<Before>(
886+
self,
887+
func: Before,
888+
) -> Cucumber<W, P, I, runner::Basic<W, F, Before, A>, Wr>
889+
where
890+
Before: for<'a> Fn(
891+
&'a gherkin::Feature,
892+
Option<&'a gherkin::Rule>,
893+
&'a gherkin::Scenario,
894+
&'a mut W,
895+
) -> LocalBoxFuture<'a, ()>,
896+
{
897+
let Self {
898+
parser,
899+
runner,
900+
writer,
901+
..
902+
} = self;
903+
Cucumber {
904+
parser,
905+
runner: runner.before(func),
906+
writer,
907+
_world: PhantomData,
908+
_parser_input: PhantomData,
909+
}
910+
}
911+
912+
/// Sets a hook, executed on each [`Scenario`] after running all its
913+
/// [`Step`]s, even after [`Skipped`] of [`Failed`] [`Step`]s.
914+
///
915+
/// Last `World` argument is supplied to the function, in case it was
916+
/// initialized before by running [`before`] hook or any non-failed
917+
/// [`Step`]. In case the last [`Scenario`]'s [`Step`] failed, we want to
918+
/// return event with an exact `World` state. Also, we don't want to impose
919+
/// additional [`Clone`] bounds on `World`, so the only option left is to
920+
/// pass [`None`] to the function.
921+
///
922+
///
923+
/// [`before`]: Self::before()
924+
/// [`Failed`]: event::Step::Failed
925+
/// [`Scenario`]: gherkin::Scenario
926+
/// [`Skipped`]: event::Step::Skipped
927+
/// [`Step`]: gherkin::Step
928+
#[must_use]
929+
pub fn after<After>(
930+
self,
931+
func: After,
932+
) -> Cucumber<W, P, I, runner::Basic<W, F, B, After>, Wr>
933+
where
934+
After: for<'a> Fn(
935+
&'a gherkin::Feature,
936+
Option<&'a gherkin::Rule>,
937+
&'a gherkin::Scenario,
938+
Option<&'a mut W>,
939+
) -> LocalBoxFuture<'a, ()>,
940+
{
941+
let Self {
942+
parser,
943+
runner,
944+
writer,
945+
..
946+
} = self;
947+
Cucumber {
948+
parser,
949+
runner: runner.after(func),
950+
writer,
951+
_world: PhantomData,
952+
_parser_input: PhantomData,
953+
}
954+
}
955+
891956
/// Replaces [`Collection`] of [`Step`]s.
892957
///
893958
/// [`Collection`]: step::Collection
@@ -1027,15 +1092,36 @@ where
10271092
{
10281093
let writer = self.filter_run(input, filter).await;
10291094
if writer.execution_has_failed() {
1095+
let mut msg = Vec::with_capacity(3);
1096+
10301097
let failed_steps = writer.failed_steps();
1098+
if failed_steps > 0 {
1099+
msg.push(format!(
1100+
"{} step{} failed",
1101+
failed_steps,
1102+
(failed_steps > 1).then(|| "s").unwrap_or_default(),
1103+
));
1104+
}
1105+
10311106
let parsing_errors = writer.parsing_errors();
1032-
panic!(
1033-
"{} step{} failed, {} parsing error{}",
1034-
failed_steps,
1035-
(failed_steps != 1).then(|| "s").unwrap_or_default(),
1036-
parsing_errors,
1037-
(parsing_errors != 1).then(|| "s").unwrap_or_default(),
1038-
);
1107+
if parsing_errors > 0 {
1108+
msg.push(format!(
1109+
"{} parsing error{}",
1110+
parsing_errors,
1111+
(parsing_errors > 1).then(|| "s").unwrap_or_default(),
1112+
));
1113+
}
1114+
1115+
let hook_errors = writer.hook_errors();
1116+
if hook_errors > 0 {
1117+
msg.push(format!(
1118+
"{} hook error{}",
1119+
hook_errors,
1120+
(hook_errors > 1).then(|| "s").unwrap_or_default(),
1121+
));
1122+
}
1123+
1124+
panic!("{}", msg.join(", "));
10391125
}
10401126
}
10411127
}

0 commit comments

Comments
 (0)