Skip to content

Commit 9e4f24a

Browse files
authored
Implement writer::Repeat (#82, #129, #133)
Additionally: - propagate Scenario Outline expansion errors - display file and line of skipped Steps - fix typos of `indent` term in source code
1 parent 03f1581 commit 9e4f24a

18 files changed

+915
-160
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ gherkin = { package = "gherkin_rust", version = "0.10" }
3939
globwalk = "0.8"
4040
itertools = "0.10"
4141
linked-hash-map = "0.5"
42+
once_cell = { version = "1.8", features = ["parking_lot"] }
4243
regex = "1.5"
4344
sealed = "0.3"
4445

book/src/Getting_Started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ fn main() {
8888

8989
If you run this, you should see an output like:
9090

91-
<script id="asciicast-t8ezu3cajA0fBkssIy04LM9pa" src="https://asciinema.org/a/t8ezu3cajA0fBkssIy04LM9pa.js" async data-autoplay="true" data-rows="24"></script>
91+
<script id="asciicast-loqmDmLvKdp4CG7URpVsLJgkB" src="https://asciinema.org/a/loqmDmLvKdp4CG7URpVsLJgkB.js" async data-autoplay="true" data-rows="23"></script>
9292

9393
You will see a checkmark next to `Given A hungry cat`, which means that test step has been matched and executed.
9494

@@ -156,7 +156,7 @@ fn feed_cat(world: &mut AnimalWorld) {
156156

157157
If you run the tests again, you'll see that two lines are green now and the next one is marked as not yet implemented:
158158

159-
<script id="asciicast-aWGpouW2F8lQRQ1O2eUQTRSlE" src="https://asciinema.org/a/aWGpouW2F8lQRQ1O2eUQTRSlE.js" async data-autoplay="true" data-rows="16"></script>
159+
<script id="asciicast-iyhXabbOv7jdKvbcsyhzqPMfo" src="https://asciinema.org/a/iyhXabbOv7jdKvbcsyhzqPMfo.js" async data-autoplay="true" data-rows="15"></script>
160160

161161
Finally: how do we validate our result? We expect that this will cause some change in the cat and that the cat will no longer be hungry since it has been fed. The `then()` step follows to assert this, as our feature says:
162162
```rust

book/src/Test_Modules_Organization.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ Avoid writing similar step definitions, as they can lead to clutter. While docum
2828

2929
As your test suit grows, it may become harder to notice how minimal changes to `regex`es can lead to mismatched `Step`s. To avoid this, we recommend using [`Cucumber::fail_on_skipped()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.fail_on_skipped) combining with `@allow_skipped` tag. This will allow you to mark out `Scenario`s which `Step`s are allowed to skip.
3030

31-
And, as time goes on, total run time of all tests can become overwhelming when you only want to test small subset of `Scenario`s. At least until you discover [`Cucumber::filter_run_and_exit()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.filter_run_and_exit), which will allow you run only `Scenario`s marked with custom [tags](https://cucumber.io/docs/cucumber/api/#tags).
31+
And, as time goes on, total run time of all tests can become overwhelming when you only want to test small subset of `Scenario`s. At least until you discover [`Cucumber::filter_run_and_exit()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.filter_run_and_exit), which will allow you run only `Scenario`s marked with custom [tags](https://cucumber.io/docs/cucumber/api/#tags).
32+
33+
We also suggest using [`Cucumber::repeat_failed()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.repeat_failed) and [`Cucumber::repeat_skipped()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.repeat_skipped) to re-output failed or skipped steps for easier navigation.

src/cucumber.rs

Lines changed: 294 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ use futures::StreamExt as _;
2424
use regex::Regex;
2525

2626
use crate::{
27-
parser, runner, step, writer, ArbitraryWriter, FailureWriter, Parser,
28-
Runner, ScenarioType, Step, World, Writer, WriterExt as _,
27+
event, parser, runner, step, writer, ArbitraryWriter, FailureWriter,
28+
Parser, Runner, ScenarioType, Step, World, Writer, WriterExt as _,
2929
};
3030

3131
/// Top-level [Cucumber] executor.
@@ -142,14 +142,14 @@ where
142142
///
143143
/// Output with a regular [`Cucumber::run()`]:
144144
/// <script
145-
/// id="asciicast-Ar8XAtrZWKMNfe7mffBXbQAFb"
146-
/// src="https://asciinema.org/a/Ar8XAtrZWKMNfe7mffBXbQAFb.js"
147-
/// async data-autoplay="true" data-rows="16">
145+
/// id="asciicast-hMyH3IYbHRFXT1yf84tXDNl2r"
146+
/// src="https://asciinema.org/a/hMyH3IYbHRFXT1yf84tXDNl2r.js"
147+
/// async data-autoplay="true" data-rows="17">
148148
/// </script>
149149
///
150150
/// To fail all the [`Skipped`] steps setup [`Cucumber`] like this:
151-
/// ```rust
152-
/// # use std::{convert::Infallible, panic::AssertUnwindSafe};
151+
/// ```rust,should_panic
152+
/// # use std::convert::Infallible;
153153
/// #
154154
/// # use async_trait::async_trait;
155155
/// # use cucumber::WorldInit;
@@ -174,7 +174,7 @@ where
174174
/// .await;
175175
/// # };
176176
/// #
177-
/// # futures::executor::block_on(AssertUnwindSafe(fut).catch_unwind());
177+
/// # futures::executor::block_on(fut);
178178
/// ```
179179
/// <script
180180
/// id="asciicast-UsaG9kMnn40nW8y4vcmXOE2tT"
@@ -222,15 +222,15 @@ where
222222
///
223223
/// Output with a regular [`Cucumber::run()`]:
224224
/// <script
225-
/// id="asciicast-Ar8XAtrZWKMNfe7mffBXbQAFb"
226-
/// src="https://asciinema.org/a/Ar8XAtrZWKMNfe7mffBXbQAFb.js"
227-
/// async data-autoplay="true" data-rows="16">
225+
/// id="asciicast-hMyH3IYbHRFXT1yf84tXDNl2r"
226+
/// src="https://asciinema.org/a/hMyH3IYbHRFXT1yf84tXDNl2r.js"
227+
/// async data-autoplay="true" data-rows="17">
228228
/// </script>
229229
///
230230
/// Adjust [`Cucumber`] to fail on all [`Skipped`] steps, but the ones
231231
/// marked with `@dog` tag:
232-
/// ```rust
233-
/// # use std::{convert::Infallible, panic::AssertUnwindSafe};
232+
/// ```rust,should_panic
233+
/// # use std::convert::Infallible;
234234
/// #
235235
/// # use async_trait::async_trait;
236236
/// # use futures::FutureExt as _;
@@ -250,12 +250,12 @@ where
250250
/// #
251251
/// # let fut = async {
252252
/// MyWorld::cucumber()
253-
/// .fail_on_skipped_with(|_, _, sc| sc.tags.iter().any(|t| t == "dog"))
253+
/// .fail_on_skipped_with(|_, _, s| !s.tags.iter().any(|t| t == "dog"))
254254
/// .run_and_exit("tests/features/readme")
255255
/// .await;
256256
/// # };
257257
/// #
258-
/// # futures::executor::block_on(AssertUnwindSafe(fut).catch_unwind());
258+
/// # futures::executor::block_on(fut);
259259
/// ```
260260
/// ```gherkin
261261
/// Feature: Animal feature
@@ -315,6 +315,285 @@ where
315315
_parser_input: PhantomData,
316316
}
317317
}
318+
319+
/// Re-outputs [`Skipped`] steps for easier navigation.
320+
///
321+
/// # Example
322+
///
323+
/// Output with a regular [`Cucumber::run()`]:
324+
/// <script
325+
/// id="asciicast-hMyH3IYbHRFXT1yf84tXDNl2r"
326+
/// src="https://asciinema.org/a/hMyH3IYbHRFXT1yf84tXDNl2r.js"
327+
/// async data-autoplay="true" data-rows="17">
328+
/// </script>
329+
///
330+
/// Adjust [`Cucumber`] to re-output all [`Skipped`] steps at the end:
331+
/// ```rust
332+
/// # use std::convert::Infallible;
333+
/// #
334+
/// # use async_trait::async_trait;
335+
/// # use futures::FutureExt as _;
336+
/// # use cucumber::WorldInit;
337+
/// #
338+
/// # #[derive(Debug, WorldInit)]
339+
/// # struct MyWorld;
340+
/// #
341+
/// # #[async_trait(?Send)]
342+
/// # impl cucumber::World for MyWorld {
343+
/// # type Error = Infallible;
344+
/// #
345+
/// # async fn new() -> Result<Self, Self::Error> {
346+
/// # Ok(Self)
347+
/// # }
348+
/// # }
349+
/// #
350+
/// # let fut = async {
351+
/// MyWorld::cucumber()
352+
/// .repeat_skipped()
353+
/// .run_and_exit("tests/features/readme")
354+
/// .await;
355+
/// # };
356+
/// #
357+
/// # futures::executor::block_on(fut);
358+
/// ```
359+
/// <script
360+
/// id="asciicast-BD1mPjYGELD6oWNKW8lTlyvDR"
361+
/// src="https://asciinema.org/a/BD1mPjYGELD6oWNKW8lTlyvDR.js"
362+
/// async data-autoplay="true" data-rows="19">
363+
/// </script>
364+
///
365+
/// [`Scenario`]: gherkin::Scenario
366+
/// [`Skipped`]: crate::event::Step::Skipped
367+
#[must_use]
368+
pub fn repeat_skipped(self) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr>> {
369+
Cucumber {
370+
parser: self.parser,
371+
runner: self.runner,
372+
writer: self.writer.repeat_skipped(),
373+
_world: PhantomData,
374+
_parser_input: PhantomData,
375+
}
376+
}
377+
378+
/// Re-outputs [`Failed`] steps for easier navigation.
379+
///
380+
/// # Example
381+
///
382+
/// Output with a regular [`Cucumber::fail_on_skipped()`]:
383+
/// ```rust,should_panic
384+
/// # use std::convert::Infallible;
385+
/// #
386+
/// # use async_trait::async_trait;
387+
/// # use futures::FutureExt as _;
388+
/// # use cucumber::WorldInit;
389+
/// #
390+
/// # #[derive(Debug, WorldInit)]
391+
/// # struct MyWorld;
392+
/// #
393+
/// # #[async_trait(?Send)]
394+
/// # impl cucumber::World for MyWorld {
395+
/// # type Error = Infallible;
396+
/// #
397+
/// # async fn new() -> Result<Self, Self::Error> {
398+
/// # Ok(Self)
399+
/// # }
400+
/// # }
401+
/// #
402+
/// # let fut = async {
403+
/// MyWorld::cucumber()
404+
/// .fail_on_skipped()
405+
/// .run_and_exit("tests/features/readme")
406+
/// .await;
407+
/// # };
408+
/// #
409+
/// # futures::executor::block_on(fut);
410+
/// ```
411+
/// <script
412+
/// id="asciicast-mDDqxWHzUaK19P0L2R2g4XRp2"
413+
/// src="https://asciinema.org/a/mDDqxWHzUaK19P0L2R2g4XRp2.js"
414+
/// async data-autoplay="true" data-rows="21">
415+
/// </script>
416+
///
417+
/// Adjust [`Cucumber`] to re-output all [`Failed`] steps at the end:
418+
/// ```rust,should_panic
419+
/// # use std::convert::Infallible;
420+
/// #
421+
/// # use async_trait::async_trait;
422+
/// # use futures::FutureExt as _;
423+
/// # use cucumber::WorldInit;
424+
/// #
425+
/// # #[derive(Debug, WorldInit)]
426+
/// # struct MyWorld;
427+
/// #
428+
/// # #[async_trait(?Send)]
429+
/// # impl cucumber::World for MyWorld {
430+
/// # type Error = Infallible;
431+
/// #
432+
/// # async fn new() -> Result<Self, Self::Error> {
433+
/// # Ok(Self)
434+
/// # }
435+
/// # }
436+
/// #
437+
/// # let fut = async {
438+
/// MyWorld::cucumber()
439+
/// .repeat_failed()
440+
/// .fail_on_skipped()
441+
/// .run_and_exit("tests/features/readme")
442+
/// .await;
443+
/// # };
444+
/// #
445+
/// # futures::executor::block_on(fut);
446+
/// ```
447+
/// <script
448+
/// id="asciicast-qKp8Hevrb6732mMUT7VduvxJc"
449+
/// src="https://asciinema.org/a/qKp8Hevrb6732mMUT7VduvxJc.js"
450+
/// async data-autoplay="true" data-rows="24">
451+
/// </script>
452+
///
453+
/// > ⚠️ __WARNING__: [`Cucumber::repeat_failed()`] should be called before
454+
/// [`Cucumber::fail_on_skipped()`], as events pass from
455+
/// outer [`Writer`]s to inner ones. So we need to
456+
/// transform [`Skipped`] to [`Failed`] first, and only
457+
/// then [`Repeat`] them.
458+
///
459+
/// [`Failed`]: crate::event::Step::Failed
460+
/// [`Repeat`]: writer::Repeat
461+
/// [`Scenario`]: gherkin::Scenario
462+
/// [`Skipped`]: crate::event::Step::Skipped
463+
#[must_use]
464+
pub fn repeat_failed(self) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr>> {
465+
Cucumber {
466+
parser: self.parser,
467+
runner: self.runner,
468+
writer: self.writer.repeat_failed(),
469+
_world: PhantomData,
470+
_parser_input: PhantomData,
471+
}
472+
}
473+
474+
/// Re-output steps by the given `filter` predicate.
475+
///
476+
/// # Example
477+
///
478+
/// Output with a regular [`Cucumber::fail_on_skipped()`]:
479+
/// ```rust,should_panic
480+
/// # use std::convert::Infallible;
481+
/// #
482+
/// # use async_trait::async_trait;
483+
/// # use futures::FutureExt as _;
484+
/// # use cucumber::WorldInit;
485+
/// #
486+
/// # #[derive(Debug, WorldInit)]
487+
/// # struct MyWorld;
488+
/// #
489+
/// # #[async_trait(?Send)]
490+
/// # impl cucumber::World for MyWorld {
491+
/// # type Error = Infallible;
492+
/// #
493+
/// # async fn new() -> Result<Self, Self::Error> {
494+
/// # Ok(Self)
495+
/// # }
496+
/// # }
497+
/// #
498+
/// # let fut = async {
499+
/// MyWorld::cucumber()
500+
/// .fail_on_skipped()
501+
/// .run_and_exit("tests/features/readme")
502+
/// .await;
503+
/// # };
504+
/// #
505+
/// # futures::executor::block_on(fut);
506+
/// ```
507+
/// <script
508+
/// id="asciicast-mDDqxWHzUaK19P0L2R2g4XRp2"
509+
/// src="https://asciinema.org/a/mDDqxWHzUaK19P0L2R2g4XRp2.js"
510+
/// async data-autoplay="true" data-rows="21">
511+
/// </script>
512+
///
513+
/// Adjust [`Cucumber`] to re-output all [`Failed`] steps ta the end by
514+
/// providing a custom `filter` predicate:
515+
/// ```rust,should_panic
516+
/// # use std::convert::Infallible;
517+
/// #
518+
/// # use async_trait::async_trait;
519+
/// # use futures::FutureExt as _;
520+
/// # use cucumber::WorldInit;
521+
/// #
522+
/// # #[derive(Debug, WorldInit)]
523+
/// # struct MyWorld;
524+
/// #
525+
/// # #[async_trait(?Send)]
526+
/// # impl cucumber::World for MyWorld {
527+
/// # type Error = Infallible;
528+
/// #
529+
/// # async fn new() -> Result<Self, Self::Error> {
530+
/// # Ok(Self)
531+
/// # }
532+
/// # }
533+
/// #
534+
/// # let fut = async {
535+
/// MyWorld::cucumber()
536+
/// .repeat_if(|ev| {
537+
/// use cucumber::event::{Cucumber, Feature, Rule, Scenario, Step};
538+
///
539+
/// matches!(
540+
/// ev,
541+
/// Ok(Cucumber::Feature(
542+
/// _,
543+
/// Feature::Rule(
544+
/// _,
545+
/// Rule::Scenario(
546+
/// _,
547+
/// Scenario::Step(_, Step::Failed(..))
548+
/// | Scenario::Background(_, Step::Failed(..))
549+
/// )
550+
/// ) | Feature::Scenario(
551+
/// _,
552+
/// Scenario::Step(_, Step::Failed(..))
553+
/// | Scenario::Background(_, Step::Failed(..))
554+
/// )
555+
/// )) | Err(_)
556+
/// )
557+
/// })
558+
/// .fail_on_skipped()
559+
/// .run_and_exit("tests/features/readme")
560+
/// .await;
561+
/// # };
562+
/// #
563+
/// # futures::executor::block_on(fut);
564+
/// ```
565+
/// <script
566+
/// id="asciicast-qKp8Hevrb6732mMUT7VduvxJc"
567+
/// src="https://asciinema.org/a/qKp8Hevrb6732mMUT7VduvxJc.js"
568+
/// async data-autoplay="true" data-rows="24">
569+
/// </script>
570+
///
571+
/// > ⚠️ __WARNING__: [`Cucumber::repeat_if()`] should be called before
572+
/// [`Cucumber::fail_on_skipped()`], as events pass from
573+
/// outer [`Writer`]s to inner ones. So we need to
574+
/// transform [`Skipped`] to [`Failed`] first, and only
575+
/// then [`Repeat`] them.
576+
///
577+
/// [`Failed`]: crate::event::Step::Failed
578+
/// [`Repeat`]: writer::Repeat
579+
/// [`Scenario`]: gherkin::Scenario
580+
/// [`Skipped`]: crate::event::Step::Skipped
581+
#[must_use]
582+
pub fn repeat_if<F>(
583+
self,
584+
filter: F,
585+
) -> Cucumber<W, P, I, R, writer::Repeat<W, Wr, F>>
586+
where
587+
F: Fn(&parser::Result<event::Cucumber<W>>) -> bool,
588+
{
589+
Cucumber {
590+
parser: self.parser,
591+
runner: self.runner,
592+
writer: self.writer.repeat_if(filter),
593+
_world: PhantomData,
594+
_parser_input: PhantomData,
595+
}
596+
}
318597
}
319598

320599
impl<W, P, I, R, Wr> Cucumber<W, P, I, R, Wr>

0 commit comments

Comments
 (0)