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

tracing integration (#213) #258

Merged
merged 39 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a61b42d
WIP
ilslv Feb 21, 2023
c063a85
Implement `tracing` events capturing and postponed output in `writer:…
ilslv Feb 22, 2023
6d9a4d7
Minor corrections
ilslv Feb 22, 2023
1358d55
Corrections, docs and clippy
ilslv Feb 23, 2023
864d1df
Add optimisation on PostponedWriter relying on fact, that we get full…
ilslv Feb 23, 2023
14af39c
Forward traces without Scenario Span to all active Scenarios
ilslv Feb 24, 2023
200c3b1
Docs and clippy
ilslv Feb 24, 2023
ea72445
Expose tracing public API to Cucumber
ilslv Feb 24, 2023
e968140
Corrections
ilslv Feb 24, 2023
5cc3068
Corrections
ilslv Feb 24, 2023
1444b28
Correct MSRV
ilslv Feb 24, 2023
e82d2ee
Merge branch 'main' into 213-tracing-integration
ilslv Feb 27, 2023
4d54d6f
tracing output test
ilslv Feb 27, 2023
3f6244d
Corrections
ilslv Feb 27, 2023
ff44f20
Corrections
ilslv Feb 27, 2023
915a30a
Corrections
ilslv Feb 27, 2023
bc323f7
Corrections
ilslv Feb 27, 2023
140f097
Corrections
ilslv Feb 27, 2023
7692a15
Corrections
ilslv Feb 27, 2023
a58cd25
Corrections
ilslv Feb 27, 2023
7fd71dc
Corrections
ilslv Feb 27, 2023
317ed7e
Corrections
ilslv Feb 27, 2023
67005d6
Corrections
ilslv Feb 27, 2023
43f5c20
WIP for fixing race between Scenario finishing and logs emitting from…
ilslv Feb 28, 2023
b080a7a
WIP for fixing race between Scenario finishing and logs emitting from…
ilslv Feb 28, 2023
860f510
WIP for fixing race between Scenario finishing and logs emitting from…
ilslv Feb 28, 2023
878e8e6
Corrections
ilslv Feb 28, 2023
42b8202
Clippy
ilslv Feb 28, 2023
620408e
Clippy
ilslv Feb 28, 2023
59a83e4
Clippy
ilslv Feb 28, 2023
23ff3f7
Clippy
ilslv Feb 28, 2023
01ff07e
Corrections
ilslv Mar 1, 2023
f63f995
Corrections and update GIFs
ilslv Mar 1, 2023
2c7ca52
Corrections
ilslv Mar 1, 2023
9f89f23
Some corrections
tyranron Mar 2, 2023
2389ba4
Replace Terminal#Manual_printing section with Terminal#Logging
ilslv Mar 3, 2023
4349578
Corrections
ilslv Mar 3, 2023
877ff3e
Explain using `print!` for `libtest`
tyranron Mar 3, 2023
8c93ff3
Corrections
tyranron Mar 3, 2023
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 @@ -81,6 +81,7 @@ jobs:
- output-json
- output-junit
- libtest
- tracing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ All user visible changes to `cucumber` crate will be documented in this file. Th



## [0.20.0] · 2023-??-??
[0.20.0]: /../../tree/v0.20.0

[Diff](/../../compare/v0.19.1...v0.20.0) | [Milestone](/../../milestone/24)

### BC Breaks

- Added `Log` variant to `event::Scenario`. ([#258])

### Added

- Added integration with `tracing` crate under `tracing` feature gate. ([#258], [#213])

[#213]: /../../issues/213
[#258]: /../../pull/258




## [0.19.1] · 2022-12-29
[0.19.1]: /../../tree/v0.19.1

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

[package.metadata.docs.rs]
all-features = true
Expand All @@ -37,12 +37,13 @@ output-json = ["dep:Inflector", "dep:serde", "dep:serde_json", "timestamps"]
output-junit = ["dep:junit-report", "timestamps"]
# Enables timestamps collecting for all events.
timestamps = []
tracing = ["dep:crossbeam-utils", "dep:tracing", "dep:tracing-subscriber"]

[dependencies]
async-trait = "0.1.43"
clap = { version = "4.0.27", features = ["derive", "wrap_help"] }
console = "0.15"
derive_more = { version = "0.99.17", features = ["as_ref", "deref", "deref_mut", "display", "error", "from", "into"], default_features = false }
derive_more = { version = "0.99.17", features = ["as_ref", "deref", "deref_mut", "display", "error", "from", "from_str", "into"], default_features = false }
drain_filter_polyfill = "0.1.2"
either = "1.6"
futures = "0.3.17"
Expand All @@ -53,6 +54,7 @@ is-terminal = "0.4.4"
itertools = "0.10"
linked-hash-map = "0.5.3"
once_cell = "1.13"
pin-project = "1.0"
regex = "1.5.5"
sealed = "0.4"
smart-default = "0.6"
Expand All @@ -71,6 +73,11 @@ Inflector = { version = "0.11", default-features = false, optional = true }
# "output-junit" feature dependencies.
junit-report = { version = "0.8", optional = true }

# "tracing" feature dependencies.
crossbeam-utils = { version = "0.8.14", optional = true }
tracing = { version = "0.1", optional = true }
tracing-subscriber = { version = "0.3.16", optional = true }

[dev-dependencies]
derive_more = "0.99.17"
rand = "0.8"
Expand All @@ -89,6 +96,11 @@ required-features = ["output-junit"]
name = "libtest"
required-features = ["libtest"]

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

[[test]]
name = "wait"
required-features = ["libtest"]
Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [JUnit XML report](output/junit.md)
- [Cucumber JSON format](output/json.md)
- [Multiple outputs](output/multiple.md)
- [`tracing` integration](output/tracing.md)
ilslv marked this conversation as resolved.
Show resolved Hide resolved
- [IntelliJ Rust integration](output/intellij.md)
- [Architecture](architecture/index.md)
- [Custom `Parser`](architecture/parser.md)
Expand Down
113 changes: 113 additions & 0 deletions book/src/output/tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
`tracing` integration
ilslv marked this conversation as resolved.
Show resolved Hide resolved
=====================

[`Cucumber::init_tracing()`] (enabled by `tracing` feature in `Cargo.toml`) initializes global [`tracing::Subscriber`] that intercepts all [`tracing` events][1] and transforms them into [`event::Scenario::Log`]s. Each [`Writer`] can handle those events in its own way. [`writer::Basic`] for example, emits all `Scenario` logs, only when `Scenario` itself is outputted:

```rust
# extern crate cucumber;
# extern crate tokio;
# extern crate tracing;
#
use std::{
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};

use cucumber::{given, then, when, World as _};
use tokio::time;

#[derive(cucumber::World, Debug, Default)]
struct World;

#[given(regex = r"(\d+) secs?")]
#[when(regex = r"(\d+) secs?")]
#[then(regex = r"(\d+) secs?")]
async fn sleep(_: &mut World, secs: u64) {
static ID: AtomicUsize = AtomicUsize::new(0);

let id = ID.fetch_add(1, Ordering::Relaxed);

tracing::info!("before {secs}s sleep: {id}");
time::sleep(Duration::from_secs(secs)).await;
tracing::info!("after {secs}s sleep: {id}");
}

#[tokio::main]
async fn main() {
World::cucumber()
.init_tracing()
.run("tests/features/wait")
.await;
}
```

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

---

Tracking which `Scenario` log is emitted in is done with [`tracing::Span`]s: each `Scenario` is executed in its own [`tracing::Span`]. In case [`tracing` event][1] is emitted outside the [`tracing::Span`] of a `Scenario`, it will be propagated to every running `Scenario`:

```rust
# extern crate cucumber;
# extern crate tokio;
# extern crate tracing;
#
# use std::{
# sync::atomic::{AtomicUsize, Ordering},
# time::Duration,
# };
#
# use cucumber::{given, then, when, World as _};
# use tokio::time;
#
# #[derive(cucumber::World, Debug, Default)]
# struct World;
#
# #[given(regex = r"(\d+) secs?")]
# #[when(regex = r"(\d+) secs?")]
# #[then(regex = r"(\d+) secs?")]
# async fn sleep(_: &mut World, secs: u64) {
# static ID: AtomicUsize = AtomicUsize::new(0);
#
# let id = ID.fetch_add(1, Ordering::Relaxed);
#
# tracing::info!("before {secs}s sleep: {id}");
# time::sleep(Duration::from_secs(secs)).await;
# tracing::info!("after {secs}s sleep: {id}");
# }
#
#[tokio::main]
async fn main() {
// Background task outside of any `Scenario`.
tokio::spawn(async {
let mut id = 0;
loop {
time::sleep(Duration::from_secs(3)).await;
tracing::info!("Background: {id}");
id += 1;
}
});


World::cucumber()
.init_tracing()
.run("tests/features/wait")
.await;
}
```

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

`Background: 2`/`3`/`4` is shown in multiple `Scenario`s.




[`Cucumber::init_tracing()`]: https://docs.rs/cucumber/latest/cucumber/struct.Cucumber.html#method.init_tracing
[`event::Scenario::Log`]: https://docs.rs/cucumber/latest/cucumber/event/enum.Scenario.html#variant.Log
[`tracing::Span`]: https://docs.rs/tracing/latest/tracing/struct.Span.html
[`tracing::Subscriber`]: https://docs.rs/tracing/latest/tracing/trait.Subscriber.html
[`Writer`]: https://docs.rs/cucumber/latest/cucumber/writer/trait.Writer.html
[`writer::Basic`]: https://docs.rs/cucumber/latest/cucumber/writer/struct.Basic.html

[1]: https://docs.rs/tracing/latest/tracing/index.html#events
Binary file added book/src/rec/tracing_basic_writer.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/rec/tracing_outside_span.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/cucumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ where
/// [`Runner`] executing [`Scenario`]s and producing [`event`]s.
///
/// [`Scenario`]: gherkin::Scenario
runner: R,
pub(crate) runner: R,

/// [`Writer`] outputting [`event`]s to some output.
writer: Wr,
Expand Down
4 changes: 4 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ pub enum Scenario<World> {
/// [`Step`] event.
Step(Arc<gherkin::Step>, Step<World>),

/// [`Scenario`] log emitted.
Log(String),

/// [`Scenario`] execution being finished.
///
/// [`Scenario`]: gherkin::Scenario
Expand All @@ -512,6 +515,7 @@ impl<World> Clone for Scenario<World> {
Self::Background(Arc::clone(bg), ev.clone())
}
Self::Step(st, ev) => Self::Step(Arc::clone(st), ev.clone()),
Self::Log(msg) => Self::Log(msg.clone()),
Self::Finished => Self::Finished,
}
}
Expand Down
124 changes: 124 additions & 0 deletions src/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

use std::{future::Future, pin::Pin, task};

use futures::{
future::{Either, FusedFuture, Then},
FutureExt as _,
};
use pin_project::pin_project;

/// Wakes the current task and returns [`task::Poll::Pending`] once.
///
/// This function is useful when we want to cooperatively give time to a task
Expand Down Expand Up @@ -31,3 +37,121 @@ impl Future for YieldNow {
}
}
}

/// Return type of [`FutureExt::then_yield()`].
type ThenYield<F, O> = Then<F, YieldThenReturn<O>, fn(O) -> YieldThenReturn<O>>;

/// [`Future`] extensions.
pub(crate) trait FutureExt: Future + Sized {
/// Yields after [`Future`] is resolved to allow other [`Future`]s to make
/// progress.
fn then_yield(self) -> ThenYield<Self, Self::Output> {
self.then(YieldThenReturn::new)
}
}

impl<T: Future> FutureExt for T {}

/// [`Future`] that returns [`task::Poll::Pending`] once and then returns the
/// value.
#[derive(Debug)]
#[pin_project]
pub(crate) struct YieldThenReturn<V> {
/// Returned value.
value: Option<V>,

/// [`YieldNow`] [`Future`].
#[pin]
r#yield: YieldNow,
ilslv marked this conversation as resolved.
Show resolved Hide resolved
}

impl<V> YieldThenReturn<V> {
/// Creates new [`YieldThenReturn`].
const fn new(v: V) -> Self {
Self {
value: Some(v),
r#yield: yield_now(),
}
}
}

impl<V> Future for YieldThenReturn<V> {
type Output = V;

fn poll(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> task::Poll<Self::Output> {
let this = self.project();
task::ready!(this.r#yield.poll(cx));
this.value
.take()
.map_or(task::Poll::Pending, task::Poll::Ready)
}
}

/// [`select`] that always [`task::Poll`]s `biased` [`Future`] first and only
/// if it returns [`task::Poll::Pending`] tries to [`task::Poll`] `regular`.
///
/// Implementation is exactly the same, as [`select`] at the moment, but
/// documentation has no guarantee about this behaviour, so can be changed.
///
/// [`select`]: futures::future::select
pub(crate) const fn select_with_biased_first<A, B>(
biased: A,
regular: B,
) -> SelectWithBiasedFirst<A, B>
where
A: Future + Unpin,
B: Future + Unpin,
{
SelectWithBiasedFirst {
inner: Some((biased, regular)),
}
}

/// [`Future`] of [`select_with_biased_first`].
pub(crate) struct SelectWithBiasedFirst<A, B> {
/// Inner [`Future`]s.
inner: Option<(A, B)>,
}

impl<A, B> Future for SelectWithBiasedFirst<A, B>
where
A: Future + Unpin,
B: Future + Unpin,
{
type Output = Either<(A::Output, B), (B::Output, A)>;

#[allow(clippy::expect_used)]
fn poll(
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> task::Poll<Self::Output> {
let (mut a, mut b) = self
.inner
.take()
.expect("cannot poll `SelectWithBiasedFirst` twice");

if let task::Poll::Ready(val) = a.poll_unpin(cx) {
return task::Poll::Ready(Either::Left((val, b)));
}

if let task::Poll::Ready(val) = b.poll_unpin(cx) {
return task::Poll::Ready(Either::Right((val, a)));
}

self.inner = Some((a, b));
task::Poll::Pending
}
}

impl<A, B> FusedFuture for SelectWithBiasedFirst<A, B>
where
A: Future + Unpin,
B: Future + Unpin,
{
fn is_terminated(&self) -> bool {
self.inner.is_none()
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ pub mod writer;

#[cfg(feature = "macros")]
pub mod codegen;
#[cfg(feature = "tracing")]
pub mod tracing;

// TODO: Remove once tests run without complains about it.
#[cfg(test)]
Expand Down
Loading