Skip to content

Commit ed082b8

Browse files
ilslvtyranron
andauthored
Pass event::ScenarioFinished to after hook (#246, #245)
Additionally: - catch panics in user code, when they happen before returned futures are polled Co-authored-by: Kai Ren <[email protected]>
1 parent d2aa9b6 commit ed082b8

File tree

8 files changed

+145
-33
lines changed

8 files changed

+145
-33
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
66

77

88

9+
## [0.17.0] · 2022-??-??
10+
[0.17.0]: /../../tree/v0.17.0
11+
12+
[Diff](/../../compare/v0.16.0...v0.17.0) | [Milestone](/../../milestone/20)
13+
14+
### BC Breaks
15+
16+
- Added `event::ScenarioFinished` as [`Cucumber::after`][0170-1] hook's argument, explaining why the `Scenario` has finished. ([#246], [#245])
17+
18+
### Fixed
19+
20+
- Uncaught panics of user code, when they happen before first poll of the returned `Future`s. ([#246])
21+
22+
[#245]: /../../discussions/245
23+
[#246]: /../../pull/246
24+
[0170-1]: https://docs.rs/cucumber/0.17.0/cucumber/struct.Cucumber.html#method.after
25+
26+
27+
28+
929
## [0.16.0] · 2022-11-09
1030
[0.16.0]: /../../tree/v0.16.0
1131

book/src/writing/hooks.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ World::cucumber()
6161
#
6262
# fn main() {
6363
World::cucumber()
64-
.after(|_feature, _rule, _scenario, _world| {
64+
.after(|_feature, _rule, _scenario, _ev, _world| {
6565
time::sleep(Duration::from_millis(300)).boxed_local()
6666
})
6767
.run_and_exit("tests/features/book");
@@ -70,12 +70,15 @@ World::cucumber()
7070

7171
> __NOTE__: [`After` hook] is enabled globally for all the executed [scenario]s. No exception is possible.
7272
73+
> __TIP__: [`After` hook] receives an [`event::ScenarioFinished`] as one of its arguments, which indicates why the [scenario] has finished (passed, failed or skipped). This information, for example, may be used to decide whether some external resources (like files) should be cleaned up if the [scenario] passes, or leaved "as is" if it fails, so helping to "freeze" the failure conditions for better investigation.
74+
7375

7476

7577

7678
[`After` hook]: https://cucumber.io/docs/cucumber/api#after
7779
[`Background`]: background.md
78-
[`Before` hook]: https://cucumber.io/docs/cucumber/api#before
80+
[`Before` hook]: https://cucumber.io/docs/cucumber/api#before
81+
[`event::ScenarioFinished`]: https://docs.rs/cucumber/*/cucumber/event/struct.ScenarioFinished.html
7982
[hook]: https://cucumber.io/docs/cucumber/api#scenario-hooks
8083
[scenario]: https://cucumber.io/docs/gherkin/reference#example
8184
[step]: https://cucumber.io/docs/gherkin/reference#steps

src/cucumber.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,7 @@ where
922922
&'a gherkin::Feature,
923923
Option<&'a gherkin::Rule>,
924924
&'a gherkin::Scenario,
925+
&'a event::ScenarioFinished,
925926
Option<&'a mut W>,
926927
) -> LocalBoxFuture<'a, ()>
927928
+ 'static,
@@ -1083,6 +1084,7 @@ where
10831084
&'a gherkin::Feature,
10841085
Option<&'a gherkin::Rule>,
10851086
&'a gherkin::Scenario,
1087+
&'a event::ScenarioFinished,
10861088
Option<&'a mut W>,
10871089
) -> LocalBoxFuture<'a, ()>
10881090
+ 'static,

src/event.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,3 +657,28 @@ impl<World> Clone for RetryableScenario<World> {
657657
}
658658
}
659659
}
660+
661+
/// Event explaining why a [Scenario] has finished.
662+
///
663+
/// [Scenario]: https://cucumber.io/docs/gherkin/reference/#example
664+
#[allow(variant_size_differences)]
665+
#[derive(Clone, Debug)]
666+
pub enum ScenarioFinished {
667+
/// [`Before`] [`Hook::Failed`].
668+
///
669+
/// [`Before`]: HookType::Before
670+
BeforeHookFailed(Info),
671+
672+
/// [`Step::Passed`].
673+
StepPassed,
674+
675+
/// [`Step::Skipped`].
676+
StepSkipped,
677+
678+
/// [`Step::Failed`].
679+
StepFailed(
680+
Option<regex::CaptureLocations>,
681+
Option<step::Location>,
682+
StepError,
683+
),
684+
}

src/runner/basic.rs

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ pub type AfterHookFn<World> = for<'a> fn(
294294
&'a gherkin::Feature,
295295
Option<&'a gherkin::Rule>,
296296
&'a gherkin::Scenario,
297+
&'a event::ScenarioFinished,
297298
Option<&'a mut World>,
298299
) -> LocalBoxFuture<'a, ()>;
299300

@@ -619,6 +620,7 @@ impl<World, Which, Before, After> Basic<World, Which, Before, After> {
619620
&'a gherkin::Feature,
620621
Option<&'a gherkin::Rule>,
621622
&'a gherkin::Scenario,
623+
&'a event::ScenarioFinished,
622624
Option<&'a mut World>,
623625
) -> LocalBoxFuture<'a, ()>,
624626
{
@@ -706,6 +708,7 @@ where
706708
&'a gherkin::Feature,
707709
Option<&'a gherkin::Rule>,
708710
&'a gherkin::Scenario,
711+
&'a event::ScenarioFinished,
709712
Option<&'a mut W>,
710713
) -> LocalBoxFuture<'a, ()>
711714
+ 'static,
@@ -873,6 +876,7 @@ async fn execute<W, Before, After>(
873876
&'a gherkin::Feature,
874877
Option<&'a gherkin::Rule>,
875878
&'a gherkin::Scenario,
879+
&'a event::ScenarioFinished,
876880
Option<&'a mut W>,
877881
) -> LocalBoxFuture<'a, ()>,
878882
{
@@ -1046,6 +1050,7 @@ where
10461050
&'a gherkin::Feature,
10471051
Option<&'a gherkin::Rule>,
10481052
&'a gherkin::Scenario,
1053+
&'a event::ScenarioFinished,
10491054
Option<&'a mut W>,
10501055
) -> LocalBoxFuture<'a, ()>,
10511056
{
@@ -1176,13 +1181,22 @@ where
11761181
}
11771182
.await;
11781183

1179-
let world = match &mut result {
1180-
Ok(world) => world.take(),
1181-
Err(exec_err) => exec_err.take_world(),
1184+
let (world, scenario_finished_ev) = match &mut result {
1185+
Ok(world) => (world.take(), event::ScenarioFinished::StepPassed),
1186+
Err(exec_err) => (
1187+
exec_err.take_world(),
1188+
exec_err.get_scenario_finished_event(),
1189+
),
11821190
};
11831191

11841192
let (world, after_hook_meta, after_hook_error) = self
1185-
.run_after_hook(world, &feature, rule.as_ref(), &scenario)
1193+
.run_after_hook(
1194+
world,
1195+
&feature,
1196+
rule.as_ref(),
1197+
&scenario,
1198+
scenario_finished_ev,
1199+
)
11861200
.await
11871201
.map_or_else(
11881202
|(w, meta, info)| (w.map(Arc::new), Some(meta), Some(info)),
@@ -1260,7 +1274,7 @@ where
12601274
retries: Option<Retries>,
12611275
) -> Result<Option<W>, ExecutionFailure<W>> {
12621276
let init_world = async {
1263-
AssertUnwindSafe(W::new())
1277+
AssertUnwindSafe(async { W::new().await })
12641278
.catch_unwind()
12651279
.await
12661280
.map_err(Info::from)
@@ -1284,12 +1298,15 @@ where
12841298
));
12851299

12861300
let fut = init_world.and_then(|mut world| async {
1287-
let fut = (hook)(
1288-
feature.as_ref(),
1289-
rule.as_ref().map(AsRef::as_ref),
1290-
scenario.as_ref(),
1291-
&mut world,
1292-
);
1301+
let fut = async {
1302+
(hook)(
1303+
feature.as_ref(),
1304+
rule.as_ref().map(AsRef::as_ref),
1305+
scenario.as_ref(),
1306+
&mut world,
1307+
)
1308+
.await;
1309+
};
12931310
match AssertUnwindSafe(fut).catch_unwind().await {
12941311
Ok(()) => Ok(world),
12951312
Err(i) => Err((Info::from(i), Some(world))),
@@ -1361,7 +1378,10 @@ where
13611378
let mut world = if let Some(w) = world {
13621379
w
13631380
} else {
1364-
match AssertUnwindSafe(W::new()).catch_unwind().await {
1381+
match AssertUnwindSafe(async { W::new().await })
1382+
.catch_unwind()
1383+
.await
1384+
{
13651385
Ok(Ok(w)) => w,
13661386
Ok(Err(e)) => {
13671387
let e = event::StepError::Panic(coerce_into_info(
@@ -1376,7 +1396,7 @@ where
13761396
}
13771397
};
13781398

1379-
match AssertUnwindSafe(step_fn(&mut world, ctx))
1399+
match AssertUnwindSafe(async { step_fn(&mut world, ctx).await })
13801400
.catch_unwind()
13811401
.await
13821402
{
@@ -1512,17 +1532,22 @@ where
15121532
feature: &Arc<gherkin::Feature>,
15131533
rule: Option<&Arc<gherkin::Rule>>,
15141534
scenario: &Arc<gherkin::Scenario>,
1535+
ev: event::ScenarioFinished,
15151536
) -> Result<
15161537
(Option<W>, Option<AfterHookEventsMeta>),
15171538
(Option<W>, AfterHookEventsMeta, Info),
15181539
> {
15191540
if let Some(hook) = self.after_hook.as_ref() {
1520-
let fut = (hook)(
1521-
feature.as_ref(),
1522-
rule.as_ref().map(AsRef::as_ref),
1523-
scenario.as_ref(),
1524-
world.as_mut(),
1525-
);
1541+
let fut = async {
1542+
(hook)(
1543+
feature.as_ref(),
1544+
rule.as_ref().map(AsRef::as_ref),
1545+
scenario.as_ref(),
1546+
&ev,
1547+
world.as_mut(),
1548+
)
1549+
.await;
1550+
};
15261551

15271552
let started = event::Metadata::new(());
15281553
let res = AssertUnwindSafe(fut).catch_unwind().await;
@@ -2201,6 +2226,23 @@ impl<W> ExecutionFailure<W> {
22012226
| Self::StepPanicked { world, .. } => world.take(),
22022227
}
22032228
}
2229+
2230+
/// Creates an [`event::ScenarioFinished`] from this [`ExecutionFailure`].
2231+
fn get_scenario_finished_event(&self) -> event::ScenarioFinished {
2232+
use event::ScenarioFinished::{
2233+
BeforeHookFailed, StepFailed, StepSkipped,
2234+
};
2235+
2236+
match self {
2237+
Self::BeforeHookPanicked { panic_info, .. } => {
2238+
BeforeHookFailed(Arc::clone(panic_info))
2239+
}
2240+
Self::StepSkipped(_) => StepSkipped,
2241+
Self::StepPanicked {
2242+
captures, loc, err, ..
2243+
} => StepFailed(captures.clone(), *loc, err.clone()),
2244+
}
2245+
}
22042246
}
22052247

22062248
#[cfg(test)]

tests/after_hook.rs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
future,
23
panic::AssertUnwindSafe,
34
sync::atomic::{AtomicUsize, Ordering},
45
time::Duration,
@@ -11,6 +12,10 @@ use tokio::time;
1112

1213
static NUMBER_OF_BEFORE_WORLDS: AtomicUsize = AtomicUsize::new(0);
1314
static NUMBER_OF_AFTER_WORLDS: AtomicUsize = AtomicUsize::new(0);
15+
static NUMBER_OF_FAILED_HOOKS: AtomicUsize = AtomicUsize::new(0);
16+
static NUMBER_OF_PASSED_STEPS: AtomicUsize = AtomicUsize::new(0);
17+
static NUMBER_OF_SKIPPED_STEPS: AtomicUsize = AtomicUsize::new(0);
18+
static NUMBER_OF_FAILED_STEPS: AtomicUsize = AtomicUsize::new(0);
1419

1520
#[tokio::main]
1621
async fn main() {
@@ -23,17 +28,28 @@ async fn main() {
2328
}
2429
.boxed()
2530
})
26-
.after(move |_, _, _, w| {
27-
async move {
28-
if w.is_some() {
29-
let after =
30-
NUMBER_OF_AFTER_WORLDS.fetch_add(1, Ordering::SeqCst);
31-
assert_ne!(after, 8, "Too much after `World`s!");
32-
} else {
33-
panic!("No World received");
34-
}
31+
.after(move |_, _, _, ev, w| {
32+
use cucumber::event::ScenarioFinished::{
33+
BeforeHookFailed, StepFailed, StepPassed, StepSkipped,
34+
};
35+
36+
match ev {
37+
BeforeHookFailed(_) => &NUMBER_OF_FAILED_HOOKS,
38+
StepPassed => &NUMBER_OF_PASSED_STEPS,
39+
StepSkipped => &NUMBER_OF_SKIPPED_STEPS,
40+
StepFailed(_, _, _) => &NUMBER_OF_FAILED_STEPS,
3541
}
36-
.boxed()
42+
.fetch_add(1, Ordering::SeqCst);
43+
44+
if w.is_some() {
45+
let after =
46+
NUMBER_OF_AFTER_WORLDS.fetch_add(1, Ordering::SeqCst);
47+
assert_ne!(after, 8, "Too much after `World`s!");
48+
} else {
49+
panic!("No `World` received");
50+
}
51+
52+
future::ready(()).boxed()
3753
})
3854
.run_and_exit("tests/features/wait");
3955

@@ -46,6 +62,10 @@ async fn main() {
4662
assert_eq!(err, "2 steps failed, 1 parsing error, 4 hook errors");
4763
assert_eq!(NUMBER_OF_BEFORE_WORLDS.load(Ordering::SeqCst), 11);
4864
assert_eq!(NUMBER_OF_AFTER_WORLDS.load(Ordering::SeqCst), 11);
65+
assert_eq!(NUMBER_OF_FAILED_HOOKS.load(Ordering::SeqCst), 2);
66+
assert_eq!(NUMBER_OF_PASSED_STEPS.load(Ordering::SeqCst), 6);
67+
assert_eq!(NUMBER_OF_SKIPPED_STEPS.load(Ordering::SeqCst), 2);
68+
assert_eq!(NUMBER_OF_FAILED_STEPS.load(Ordering::SeqCst), 2);
4969
}
5070

5171
#[given(regex = r"(\d+) secs?")]

tests/json.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async fn main() {
2828
}
2929
.boxed_local()
3030
})
31-
.after(|_, _, sc, _| {
31+
.after(|_, _, sc, _, _| {
3232
async {
3333
assert!(!sc.tags.iter().any(|t| t == "fail_after"), "Tag!");
3434
}

tests/wait.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async fn main() {
2828
}
2929
.boxed_local()
3030
})
31-
.after(move |_, _, _, _| time::sleep(cli.custom.pause).boxed_local())
31+
.after(move |_, _, _, _, _| time::sleep(cli.custom.pause).boxed_local())
3232
.with_writer(writer::Libtest::or_basic())
3333
.with_cli(cli)
3434
.run_and_exit("tests/features/wait");

0 commit comments

Comments
 (0)