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

Support libtest's format=json unstable feature (#194, #203, #218) #220

Merged
merged 30 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dd7eeb3
Replace WorldInit macro with World
ilslv Jul 7, 2022
0f4a869
Docs
ilslv Jul 7, 2022
f9f724f
CHANGELOG
ilslv Jul 7, 2022
1ba639b
Fix doctest
ilslv Jul 7, 2022
191c08f
Remove WorldInit trait and move all of it's methods into the World trait
ilslv Jul 7, 2022
0d7c2ac
Add event::Cucumber::ParsingFinished variant
ilslv Jul 8, 2022
7a2ad47
WIP
ilslv Jul 8, 2022
67d0888
Merge branch 'main' into libtest-format-json
ilslv Jul 8, 2022
65b5247
Docs and clean ups
ilslv Jul 11, 2022
801c37c
Add IntelliJ Rust integration book chapter and CHANGELOG
ilslv Jul 11, 2022
ec2a971
Remove comment
ilslv Jul 11, 2022
207d569
Corrections
ilslv Jul 11, 2022
916d3e1
Clippy
ilslv Jul 11, 2022
977bc8a
Corrections
ilslv Jul 11, 2022
bd2ea5e
Docs
ilslv Jul 11, 2022
0f87bbe
Corrections
ilslv Jul 12, 2022
a80f853
Minor corrections [skip ci]
tyranron Jul 12, 2022
aeab036
Corrections [run ci]
ilslv Jul 15, 2022
f53bd4d
Docs [run ci]
ilslv Jul 15, 2022
0c01bc1
Docs [run ci]
ilslv Jul 15, 2022
4794a6f
Add commands for creating rust issue [run ci]
ilslv Jul 15, 2022
78af231
Nevermind, already reported [run ci]
ilslv Jul 15, 2022
60be14c
Fix book [run ci]
ilslv Jul 15, 2022
5c0aca8
Fix book [run ci]
ilslv Jul 15, 2022
5ba5d17
Fix book [run ci]
ilslv Jul 15, 2022
d48442a
Merge branch 'main' into libtest-format-json
tyranron Jul 18, 2022
e563633
Some corrections [skip ci]
tyranron Jul 18, 2022
fa70d42
Corrections [run ci]
ilslv Jul 19, 2022
30c9be8
Bump async-trait [run ci]
ilslv Jul 19, 2022
bac2b75
Corrections [run ci]
tyranron Jul 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ jobs:
- timestamps
- output-json
- output-junit
- libtest
ilslv marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
- Bumped up [MSRV] to 1.62 for more clever support of [Cargo feature]s and simplified codegen. ([fbd08ec2], [cf055ac0], [8ad5cc86])
- Replaced `#[derive(WorldInit)]` with `#[derive(World)]` to remove the need of manual `World` trait implementation. ([#219], [#217])
- Merged `WorldInit` trait into the `World` trait. ([#219])
- Added `ParsingFinished` variant to `event::Cucumber`. ([#220])

### Added

- `writer::Or` to alternate between 2 `Writer`s. ([#220])
- `writer::Libtest` (enables [IntelliJ Rust integration][0140-1]) behind the `libtest` feature flag (enabled by default). ([#220])
- `FeatureExt::count_steps()` method. ([#220])

### Changed

Expand All @@ -25,9 +32,11 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
[#216]: /../../pull/216
[#217]: /../../issues/217
[#219]: /../../pull/219
[#220]: /../../pull/220
[8ad5cc86]: /../../commit/8ad5cc866bb9d6b49470790e3b0dd40690f63a09
[cf055ac0]: /../../commit/cf055ac06c7b72f572882ce15d6a60da92ad60a0
[fbd08ec2]: /../../commit/fbd08ec24dbd036c89f5f0af4d936b616790a166
[0140-1]: book/src/output/intellij.md



Expand Down
11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ repository = "https://github.com/cucumber-rs/cucumber"
readme = "README.md"
categories = ["asynchronous", "development-tools::testing"]
keywords = ["cucumber", "testing", "bdd", "atdd", "async"]
include = ["/src/", "/tests/after_hook.rs", "/tests/json.rs", "/tests/junit.rs", "/tests/wait.rs", "/LICENSE-*", "/README.md", "/CHANGELOG.md"]
include = ["/src/", "/tests/after_hook.rs", "/tests/json.rs", "/tests/junit.rs", "/tests/libtest.rs", "/tests/wait.rs", "/LICENSE-*", "/README.md", "/CHANGELOG.md"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[features]
default = ["macros"]
default = ["libtest", "macros"]
ilslv marked this conversation as resolved.
Show resolved Hide resolved
# Enables step attributes and auto-wiring.
macros = ["dep:anyhow", "dep:cucumber-codegen", "dep:cucumber-expressions", "dep:inventory"]
# Enables support for outputting in libtest's JSON format.
libtest = ["dep:serde", "dep:serde_json", "timestamps"]
# Enables support for outputting in Cucumber JSON format.
output-json = ["dep:Inflector", "dep:serde", "dep:serde_json", "timestamps"]
# Enables support for outputting JUnit XML report.
Expand Down Expand Up @@ -86,6 +88,11 @@ name = "junit"
required-features = ["output-junit"]
harness = false

[[test]]
name = "libtest"
required-features = ["libtest"]
harness = false

[[test]]
name = "wait"
harness = false
Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- [JUnit XML report](output/junit.md)
- [Cucumber JSON format](output/json.md)
- [Multiple outputs](output/multiple.md)
- [IntelliJ Rust integration](output/intellij.md)
- [Architecture](architecture/index.md)
- [Custom `Parser`](architecture/parser.md)
- [Custom `Runner`](architecture/runner.md)
Expand Down
19 changes: 19 additions & 0 deletions book/src/output/intellij.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
IntelliJ Rust
=============

With feature `libtest` (enabled by default), IDE with [IntelliJ Rust] plugin can interpret output of [`cucumber`] test similar to unit tests. To use it, just add [Cargo configuration] (current example uses `cargo test --test wait` command) or run it via [Cargo command]. This automatically adds `--format=json` CLI option, which makes [`cucumber`] output IDE-compatible.

![record](../rec/intellij.gif)

> __NOTE__: There are currently 2 caveats with [IntelliJ Rust] integration:
> 1. Because of [output interpretation issue], current timing reports for individual tests are accurate only for serial tests (or in case `--concurrency=1` CLI option is used);
> 2. Although debugger works, test window may select `Step` that didn't trigger the breakpoint. To fix this, use `--concurrency=1` CLI option.




[`cucumber`]: https://docs.rs/cucumber
[output interpretation issue]: https://github.com/intellij-rust/intellij-rust/issues/9041
[Cargo command]: https://plugins.jetbrains.com/plugin/8182-rust/docs/cargo-command-configuration.html
[Cargo configuration]: https://plugins.jetbrains.com/plugin/8182-rust/docs/rust-testing.html
[IntelliJ Rust]: https://www.jetbrains.com/rust/
Binary file added book/src/rec/intellij.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions codegen/tests/two_worlds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ async fn main() {
.run("./tests/features")
.await;

assert_eq!(writer.steps.passed, 7);
assert_eq!(writer.steps.skipped, 5);
assert_eq!(writer.steps.failed, 0);
assert_eq!(writer.left.steps.passed, 7);
assert_eq!(writer.left.steps.skipped, 5);
assert_eq!(writer.left.steps.failed, 0);
ilslv marked this conversation as resolved.
Show resolved Hide resolved

let writer = SecondWorld::cucumber()
.max_concurrent_scenarios(None)
.run("./tests/features")
.await;

assert_eq!(writer.steps.passed, 1);
assert_eq!(writer.steps.skipped, 8);
assert_eq!(writer.steps.failed, 0);
assert_eq!(writer.left.steps.passed, 1);
assert_eq!(writer.left.steps.skipped, 8);
assert_eq!(writer.left.steps.failed, 0);
}
54 changes: 45 additions & 9 deletions src/cucumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,16 +368,7 @@ where
_parser_input: PhantomData,
}
}
}

impl<W, P, I, R, Wr, Cli> Cucumber<W, P, I, R, Wr, Cli>
where
W: World,
P: Parser<I>,
R: Runner<W>,
Wr: Writer<W> + for<'val> writer::Arbitrary<'val, W, String>,
Cli: clap::Args,
{
/// Consider [`Skipped`] [`Background`] or regular [`Step`]s as [`Failed`]
/// if their [`Scenario`] isn't marked with `@allow.skipped` tag.
///
Expand Down Expand Up @@ -792,6 +783,7 @@ where
}
}

#[cfg(not(feature = "libtest"))]
/// Shortcut for the [`Cucumber`] type returned by its [`Default`] impl.
pub(crate) type DefaultCucumber<W, I> = Cucumber<
W,
Expand All @@ -801,6 +793,7 @@ pub(crate) type DefaultCucumber<W, I> = Cucumber<
writer::Summarize<writer::Normalize<W, writer::Basic>>,
>;

#[cfg(not(feature = "libtest"))]
impl<W, I> Default for DefaultCucumber<W, I>
where
W: World + Debug,
Expand All @@ -815,6 +808,49 @@ where
}
}

#[cfg(feature = "libtest")]
// TODO: Maybe remove normalization from `writer::Libtest`, once resolved:
// https://github.com/intellij-rust/intellij-rust/issues/9041
/// Shortcut for the [`Cucumber`] type returned by its [`Default`] impl.
pub(crate) type DefaultCucumber<W, I> = Cucumber<
W,
parser::Basic,
I,
runner::Basic<W>,
writer::Or<
writer::Summarize<writer::Normalize<W, writer::Basic>>,
writer::Normalize<W, writer::Libtest<W>>,
fn(
&parser::Result<Event<event::Cucumber<W>>>,
&cli::Compose<writer::basic::Cli, writer::libtest::Cli>,
) -> bool,
>,
>;

#[cfg(feature = "libtest")]
impl<W, I> Default for DefaultCucumber<W, I>
where
W: World + Debug,
I: AsRef<Path>,
{
fn default() -> Self {
Self::custom(
parser::Basic::new(),
runner::Basic::default(),
writer::Or::new(
writer::Basic::stdout().summarized(),
writer::Libtest::stdout().normalized(),
|_, cli| {
!matches!(
cli.right.format,
Some(writer::libtest::Format::Json),
)
},
),
)
}
}

impl<W, I> DefaultCucumber<W, I>
where
W: World + Debug,
Expand Down
44 changes: 44 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ impl Metadata {
/// Top-level [Cucumber] run event.
///
/// [Cucumber]: https://cucumber.io
#[allow(variant_size_differences)]
#[derive(Debug)]
pub enum Cucumber<World> {
/// [`Cucumber`] execution being started.
Expand All @@ -126,6 +127,36 @@ pub enum Cucumber<World> {
/// [`Feature`] event.
Feature(Arc<gherkin::Feature>, Feature<World>),

/// All [`Feature`]s are parsed.
///
/// [`Feature`]: gherkin::Feature
ParsingFinished {
/// Number of [`Feature`]s.
///
/// [`Feature`]: gherkin::Feature
features: usize,

/// Number of [`Rule`]s.
///
/// [`Rule`]: gherkin::Rule
rules: usize,

/// Number of [`Scenario`]s.
///
/// [`Scenario`]: gherkin::Scenario
scenarios: usize,

/// Number of [`Step`]s.
///
/// [`Step`]: gherkin::Step
steps: usize,

/// Number of [`Parser`] errors.
///
/// [`Parser`]: crate::Parser
parser_errors: usize,
},

/// [`Cucumber`] execution being finished.
Finished,
}
Expand All @@ -137,6 +168,19 @@ impl<World> Clone for Cucumber<World> {
match self {
Self::Started => Self::Started,
Self::Feature(f, ev) => Self::Feature(Arc::clone(f), ev.clone()),
Self::ParsingFinished {
features,
rules,
scenarios,
steps,
parser_errors,
} => Self::ParsingFinished {
features: *features,
rules: *rules,
scenarios: *scenarios,
steps: *steps,
parser_errors: *parser_errors,
},
Self::Finished => Self::Finished,
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ pub trait Ext: Sized {
/// [`Scenario`]: gherkin::Scenario
#[must_use]
fn count_scenarios(&self) -> usize;

/// Counts all the [`Feature`]'s [`Step`]s.
///
/// [`Feature`]: gherkin::Feature
/// [`Step`]: gherkin::Step
#[must_use]
fn count_steps(&self) -> usize;
}

#[sealed]
Expand All @@ -107,6 +114,16 @@ impl Ext for gherkin::Feature {
self.scenarios.len()
+ self.rules.iter().map(|r| r.scenarios.len()).sum::<usize>()
}

fn count_steps(&self) -> usize {
self.scenarios.iter().map(|s| s.steps.len()).sum::<usize>()
+ self
.rules
.iter()
.flat_map(|r| &r.scenarios)
.map(|s| s.steps.len())
.sum::<usize>()
}
}

/// Expands [`Scenario`] [`Examples`], if any.
Expand Down
33 changes: 29 additions & 4 deletions src/runner/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ where
/// [`Feature`]: gherkin::Feature
async fn insert_features<W, S, F>(
into: Features,
features: S,
features_stream: S,
which_scenario: F,
sender: mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,
fail_fast: bool,
Expand All @@ -480,20 +480,45 @@ async fn insert_features<W, S, F>(
) -> ScenarioType
+ 'static,
{
pin_mut!(features);
while let Some(feat) = features.next().await {
let mut features = 0;
let mut rules = 0;
let mut scenarios = 0;
let mut steps = 0;
let mut parser_errors = 0;

pin_mut!(features_stream);
while let Some(feat) = features_stream.next().await {
match feat {
Ok(f) => into.insert(f, &which_scenario).await,
Ok(f) => {
features += 1;
rules += f.rules.len();
scenarios += f.count_scenarios();
steps += f.count_steps();

into.insert(f, &which_scenario).await;
}
// If the receiver end is dropped, then no one listens for events
// so we can just stop from here.
Err(e) => {
parser_errors += 1;

if sender.unbounded_send(Err(e)).is_err() || fail_fast {
break;
}
}
}
}

drop(sender.unbounded_send(Ok(Event::new(
event::Cucumber::ParsingFinished {
features,
rules,
scenarios,
steps,
parser_errors,
},
))));

into.finish();
}

Expand Down
6 changes: 5 additions & 1 deletion src/writer/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ where

match ev.map(Event::into_inner) {
Err(err) => self.parsing_failed(&err),
Ok(Cucumber::Started | Cucumber::Finished) => Ok(()),
Ok(
Cucumber::Started
| Cucumber::ParsingFinished { .. }
| Cucumber::Finished,
) => Ok(()),
Ok(Cucumber::Feature(f, ev)) => match ev {
Feature::Started => self.feature_started(&f),
Feature::Scenario(sc, ev) => self.scenario(&f, &sc, &ev),
Expand Down
3 changes: 2 additions & 1 deletion src/writer/fail_on_skipped.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ where
Option<&gherkin::Rule>,
&gherkin::Scenario,
) -> bool,
Wr: for<'val> writer::Arbitrary<'val, W, String>,
Wr: Writer<W>,
ilslv marked this conversation as resolved.
Show resolved Hide resolved
{
type Cli = Wr::Cli;

Expand Down Expand Up @@ -118,6 +118,7 @@ where
) => map_failed_step(f, None, sc, st),
Cucumber::Started
| Cucumber::Feature(..)
| Cucumber::ParsingFinished { .. }
ilslv marked this conversation as resolved.
Show resolved Hide resolved
| Cucumber::Finished => ev,
})
});
Expand Down
2 changes: 1 addition & 1 deletion src/writer/junit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ where

match ev.map(Event::split) {
Err(err) => self.handle_error(&err),
Ok((Cucumber::Started, _)) => {}
Ok((Cucumber::Started | Cucumber::ParsingFinished { .. }, _)) => {}
Ok((Cucumber::Feature(feat, ev), meta)) => match ev {
Feature::Started => {
self.suit = Some(
Expand Down
Loading