diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09437a85..30969109 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,7 @@ jobs: - output-json - output-junit - libtest + - tracing runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b386c1..fe8d3ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + +- [`tracing`] crate integration behind the `tracing` feature flag. ([#213], [#258]) + +[#213]: /../../issues/213 +[#258]: /../../pull/258 + + + + ## [0.19.1] · 2022-12-29 [0.19.1]: /../../tree/v0.19.1 @@ -680,6 +699,7 @@ All user visible changes to `cucumber` crate will be documented in this file. Th [`clap`]: https://docs.rs/clap [`gherkin`]: https://docs.rs/gherkin [`gherkin_rust`]: https://docs.rs/gherkin_rust +[`tracing`]: https://docs.rs/tracing [Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html [Cucumber Expressions]: https://cucumber.github.io/cucumber-expressions diff --git a/Cargo.toml b/Cargo.toml index b01c278b..992aba6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 @@ -37,12 +37,14 @@ output-json = ["dep:Inflector", "dep:serde", "dep:serde_json", "timestamps"] output-junit = ["dep:junit-report", "timestamps"] # Enables timestamps collecting for all events. timestamps = [] +# Enables integraion with `tracing` crate. +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" @@ -53,6 +55,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" @@ -71,6 +74,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" @@ -89,6 +97,11 @@ required-features = ["output-junit"] name = "libtest" required-features = ["libtest"] +[[test]] +name = "tracing" +required-features = ["tracing"] +harness = false + [[test]] name = "wait" required-features = ["libtest"] diff --git a/README.md b/README.md index 853b1995..3a4c6054 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ For more examples check out the Book ([current][1] | [edge][2]). - `output-json` (implies `timestamps`): Enables support for outputting in [Cucumber JSON format]. - `output-junit` (implies `timestamps`): Enables support for outputting [JUnit XML report]. - `libtest` (implies `timestamps`): Enables compatibility with [Rust `libtest`][4]'s JSON output format. Useful for [IntelliJ Rust plugin integration][3]. +- `tracing`: Enables [integration with `tracing` crate][5]. @@ -129,5 +130,6 @@ at your option. [2]: https://cucumber-rs.github.io/cucumber/main [3]: https://cucumber-rs.github.io/cucumber/main/output/intellij.html [4]: https://doc.rust-lang.org/rustc/tests/index.html +[5]: https://cucumber-rs.github.io/cucumber/main/output/tracing.html [asciicast]: data:image/gif;base64,R0lGODlhVAj4AfUAABITFMzMzCMjJBobHENERMTExJeYmLy8vDc4OVNUVJycnRYXGKusrKSkpLO0tF9gYGtsbISEhX9/gEdHSIuMjHd4eI+QkGdnaHN0dDs8PU6+Ikq0IR85FyZMGT+UHzyMHhglFTZ7HS5jGkiuIUWkIBUbFStcGjFsGx00FxsrFiNEGEKcHzmEHUOgHySixSaw1xc9SCat0xlNWx1pfyGMqiScvhxecSWmyh50jCOXuCKTsyGFoiWpziB/mh96lAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQEKAD/ACwAAAAAVAj4AQAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AA6ILQLCgQYMCEypcyLChw4cQoRycWDCixYsYM2rcyFEeRYodQ4ocSbKkyZOyPv9OXICypcuXMGPKnBlH5UGaOHPq3Mmzp84AAmjYROizqNGjSJMqpQdU6FCCS6NKnUq1qlViTWc8hXq1q9evYMOKfZTAQAGCDgw8iLE1wNi3cOPKnUvXiwWCDQIoYMD1Rdu6gAMLHky4MIsEXJsiqMB2q+HHkCNLnoz0LgQRWWX8pcy5s+fPoB+aXTsgc+OnA0KrXs26tetyEvSyNA1j8+vbuHPr3g0MAV4IARA4Rc27uPHjyJN7mnDAoIIEmh0rn069uvXrfB5QcJ7ar3Ts4MOLH08+S/AHZxl7J16+vfv38OPXaIq4QO3v8vPr388fvOK+LdjW34AEFmggZ0GN8J//fesNxdKBEEYo4YRgGRCBcKUJIABfEdzHHoUghijiiDMpgBZfDuCVoAsCkujiizDG+FBZKf7WXYM2PSjjjjz26GM9tOGo0o9EFmnkkd8EySJ+SDbp5JNQCqNkgExGaeWVWGaJymXDOajll2CGKWYqLY5p5plopslImWq26eabcMrBZpx01mnnnVjMieeefPbp55+ABirooIQWauihiCaq6KKMNuroo5BGKumklFZq6aWYZqrpppx26umnoIYq6qiklmrqqaimquqqrLbq6quwxirrrLTWauutuOaq66689urrr8AGK+ywxBZr7LHIJqvsssw26+yz0EYr7bTUVmvt/7XYZqvtttx26+234IYr7rjklmvuueimq+667Lbr7rvwxivvvPTWa++9+Oar77789uvvvwAHLPDABBds8MEIJ6zwwgw37PDDEEcs8cQUV2zxxRhnrPHGHHfs8ccghyzyyCSXbPLJKKes8sost+zyyzDHLPPMNNds880456zzzjz37PPPQAct9NBEF2300UgnrfTSTDft9NNQRy311FRXbfXVWGet9dZcd+3112CHLfbYZJdt9tlop6322my37fbbcMct99x012333XjnrffefPft99+ABy744IQXbvjhiCeu+OKMN+7445BHLvnklFdu+eWYZ6755px37vnnoIcu+v/opJdu+umop6766qy37vrrsMcu++y012777bjnrvvuvPfu++/ABy/88MQXb/zxyCev/PLMN+/889BHL/301Fdv/fXYZ6/99tx37/334Icv/vjkl2/++einr/767Lfv/vvwxy///PTXb//9+Oev//789+///wAMoAAHSMACGvCACEygAhfIwAY68IEQjKAEJ0jBClrwghjMoAY3yMEOevCDIAyhCEdIwhKa8IQoTKEKV8jCFrrwhTCMoQxnSMMa2vCGOMyhDnfIwx768IdADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMb/MprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWuMylLnfJy1768pfADKYwh0nMYhrzmMhMpjKXycxmOvOZ0IymNKdJzWpa85rYzKY2t8nNbnrzm+AMpzjHSc5ymvOc6EynOtfJzna6853wjKc850nPetrznvjMpz73yc9++vOfAA2oQAdK0IIa9KAITahCF8rQhjr0oRCNqEQnStGKWvSiGM2oRjfK0Y56//SjIA2pSEdK0pKa9KQoTalKV8rS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 diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index dd5a3e45..2256670b 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -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) - [IntelliJ Rust integration](output/intellij.md) - [Architecture](architecture/index.md) - [Custom `Parser`](architecture/parser.md) diff --git a/book/src/output/index.md b/book/src/output/index.md index 1d0c3592..8f3cc6a1 100644 --- a/book/src/output/index.md +++ b/book/src/output/index.md @@ -7,4 +7,5 @@ This chapter describes possible way and tweaks of outputting test suite results. 2. [JUnit XML report](junit.md) 3. [Cucumber JSON format](json.md) 4. [Multiple outputs](multiple.md) -5. [IntelliJ Rust integration](intellij.md) +5. [`tracing` integration](tracing.md) +6. [IntelliJ Rust integration](intellij.md) diff --git a/book/src/output/terminal.md b/book/src/output/terminal.md index d1ed8449..7579dcfc 100644 --- a/book/src/output/terminal.md +++ b/book/src/output/terminal.md @@ -213,9 +213,9 @@ async fn main() { -## Manual printing +## Debug printing and/or logging -Though [`cucumber`] crate doesn't capture any manual printing produced in a [step] matching function (such as [`dbg!`] or [`println!`] macros), it may be [quite misleading][#177] to produce and use it for debugging purposes. The reason is simply because [`cucumber`] crate executes [scenario]s concurrently and [normalizes][3] their results before outputting, while any manual print is produced instantly at the moment of its [step] execution. +Though [`cucumber`] crate doesn't capture any manual debug printing produced in a [step] matching function (such as [`dbg!`] or [`println!`] macros), it may be [quite misleading][#177] to produce and use it for debugging purposes. The reason is simply because [`cucumber`] crate executes [scenario]s concurrently and [normalizes][3] their results before outputting, while any manual print is produced instantly at the moment of its [step] execution. > __WARNING:__ Moreover, manual printing will very likely interfere with [default][1] interactive pretty-printing. @@ -348,7 +348,48 @@ async fn main() { ``` ![record](../rec/output_terminal_custom.gif) -> __NOTE__: The custom print is still output before its [step], because is printed during the [step] execution. +> __NOTE__: The custom print is still output before its [step], because is printed during the [step] execution. + +Much better option for debugging would be using [`tracing` crate integration](tracing.md) instead of [`dbg!`]/[`println!`] for doing logs. + +```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) diff --git a/book/src/output/tracing.md b/book/src/output/tracing.md new file mode 100644 index 00000000..54e30eef --- /dev/null +++ b/book/src/output/tracing.md @@ -0,0 +1,119 @@ +`tracing` integration +===================== + +[`Cucumber::init_tracing()`] (enabled by `tracing` feature in `Cargo.toml`) initializes global [`tracing::Subscriber`] that intercepts all the [`tracing` events][1] and transforms them into [`event::Scenario::Log`]s. Each [`Writer`] can handle those [`event::Scenario::Log`]s in its own way. [`writer::Basic`], for example, emits all the [`event::Scenario::Log`]s only whenever [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) + + + + +## Loosing [`tracing::Span`] + +[`tracing::Span`] is used to wire emitted [`tracing` events][1] (logs) to concrete [scenario]s: each [scenario] is executed in its own [`tracing::Span`]. In case a [`tracing` event][1] is emitted outside the [`tracing::Span`] of a [scenario], it will be propagated to every running [scenario] at the moment. + +```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) + +As we see, `Background: 2`/`3`/`4` is shown in multiple [scenario]s, while being emitted only once each. + +> __TIP__: If you're [`spawn`]ing a [`Future`] inside your [step] matching function, consider to [propagate][2] its [`tracing::Span`] into the [`spawn`]ed [`Future`] for outputting its logs properly. + + + + +[`Cucumber::init_tracing()`]: https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.init_tracing +[`event::Scenario::Log`]: https://docs.rs/cucumber/*/cucumber/event/enum.Scenario.html#variant.Log +[`Future`]: https://doc.rust-lang.org/stable/std/future/trait.Future.html +[`spawn`]: https://docs.rs/tokio/*/tokio/fn.spawn.html +[`tracing::Span`]: https://docs.rs/tracing/*/tracing/struct.Span.html +[`tracing::Subscriber`]: https://docs.rs/tracing/*/tracing/trait.Subscriber.html +[`Writer`]: https://docs.rs/cucumber/*/cucumber/writer/trait.Writer.html +[`writer::Basic`]: https://docs.rs/cucumber/*/cucumber/writer/struct.Basic.html +[scenario]: https://cucumber.io/docs/gherkin/reference#example +[step]: https://cucumber.io/docs/gherkin/reference#steps +[1]: https://docs.rs/tracing/*/tracing/index.html#events +[2]: https://docs.rs/tracing/*/tracing/struct.Span.html#method.enter diff --git a/book/src/rec/tracing_basic_writer.gif b/book/src/rec/tracing_basic_writer.gif new file mode 100644 index 00000000..6a61d498 Binary files /dev/null and b/book/src/rec/tracing_basic_writer.gif differ diff --git a/book/src/rec/tracing_outside_span.gif b/book/src/rec/tracing_outside_span.gif new file mode 100644 index 00000000..2f7ba192 Binary files /dev/null and b/book/src/rec/tracing_outside_span.gif differ diff --git a/src/cucumber.rs b/src/cucumber.rs index 074d3e0f..eedc2d7f 100644 --- a/src/cucumber.rs +++ b/src/cucumber.rs @@ -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, diff --git a/src/event.rs b/src/event.rs index 568c83de..4ac255b0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -495,6 +495,9 @@ pub enum Scenario { /// [`Step`] event. Step(Arc, Step), + /// [`Scenario`]'s log entry is emitted. + Log(String), + /// [`Scenario`] execution being finished. /// /// [`Scenario`]: gherkin::Scenario @@ -512,6 +515,7 @@ impl Clone for Scenario { 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, } } diff --git a/src/future.rs b/src/future.rs index 68d36ab1..47edcceb 100644 --- a/src/future.rs +++ b/src/future.rs @@ -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 @@ -31,3 +37,121 @@ impl Future for YieldNow { } } } + +/// Return type of a [`FutureExt::then_yield()`] method. +type ThenYield = Then, fn(O) -> YieldThenReturn>; + +/// Extensions of a [`Future`], used inside this crate. +pub(crate) trait FutureExt: Future + Sized { + /// Yields after this [`Future`] is resolved allowing other [`Future`]s + /// making progress. + fn then_yield(self) -> ThenYield { + self.then(YieldThenReturn::new) + } +} + +impl FutureExt for T {} + +/// [`Future`] returning a [`task::Poll::Pending`] once, before returning a +/// contained value. +#[derive(Debug)] +#[pin_project] +pub(crate) struct YieldThenReturn { + /// Value to be returned. + value: Option, + + /// [`YieldNow`] [`Future`]. + r#yield: YieldNow, +} + +impl YieldThenReturn { + /// Creates a new [`YieldThenReturn`] [`Future`]. + const fn new(v: V) -> Self { + Self { + value: Some(v), + r#yield: yield_now(), + } + } +} + +impl Future for YieldThenReturn { + type Output = V; + + fn poll( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll { + let this = self.project(); + task::ready!(this.r#yield.poll_unpin(cx)); + this.value + .take() + .map_or(task::Poll::Pending, task::Poll::Ready) + } +} + +/// [`select`] that always [`poll()`]s the `biased` [`Future`] first, and only +/// if it returns [`task::Poll::Pending`] tries to [`poll()`] the `regular` one. +/// +/// Implementation is exactly the same, as [`select`] at the moment, but +/// documentation has no guarantees about this behaviour, so can be changed. +/// +/// [`poll()`]: Future::poll +/// [`select`]: futures::future::select +pub(crate) const fn select_with_biased_first( + biased: A, + regular: B, +) -> SelectWithBiasedFirst +where + A: Future + Unpin, + B: Future + Unpin, +{ + SelectWithBiasedFirst { + inner: Some((biased, regular)), + } +} + +/// [`Future`] returned by a [`select_with_biased_first()`] function. +pub(crate) struct SelectWithBiasedFirst { + /// Inner [`Future`]s. + inner: Option<(A, B)>, +} + +impl Future for SelectWithBiasedFirst +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 { + 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 FusedFuture for SelectWithBiasedFirst +where + A: Future + Unpin, + B: Future + Unpin, +{ + fn is_terminated(&self) -> bool { + self.inner.is_none() + } +} diff --git a/src/lib.rs b/src/lib.rs index a998db15..58a0f747 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)] diff --git a/src/runner/basic.rs b/src/runner/basic.rs index 800dee02..22fed327 100644 --- a/src/runner/basic.rs +++ b/src/runner/basic.rs @@ -17,13 +17,16 @@ use std::{ ops::ControlFlow, panic::{self, AssertUnwindSafe}, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }, thread, time::{Duration, Instant}, }; +#[cfg(feature = "tracing")] +use crossbeam_utils::atomic::AtomicCell; +use derive_more::{Display, FromStr}; use drain_filter_polyfill::VecExt; use futures::{ channel::{mpsc, oneshot}, @@ -38,10 +41,12 @@ use gherkin::tagexpr::TagOperation; use itertools::Itertools as _; use regex::{CaptureLocations, Regex}; +#[cfg(feature = "tracing")] +use crate::tracing::{Collector as TracingCollector, SpanCloseWaiter}; use crate::{ event::{self, HookType, Info, Retries}, feature::Ext as _, - future::yield_now, + future::{select_with_biased_first, FutureExt as _}, parser, step, tag::Ext as _, Event, Runner, Step, World, @@ -378,8 +383,18 @@ pub struct Basic< /// Indicates whether execution should be stopped after the first failure. fail_fast: bool, + + #[cfg(feature = "tracing")] + /// [`TracingCollector`] for [`event::Scenario::Log`]s forwarding. + pub(crate) logs_collector: Arc>>>, } +#[cfg(feature = "tracing")] +/// Assertion that [`Basic::logs_collector`] [`AtomicCell::is_lock_free`]. +const _: () = { + assert!(AtomicCell::>>::is_lock_free()); +}; + // Implemented manually to omit redundant `World: Clone` trait bound, imposed by // `#[derive(Clone)]`. impl Clone for Basic { @@ -395,6 +410,8 @@ impl Clone for Basic { before_hook: self.before_hook.clone(), after_hook: self.after_hook.clone(), fail_fast: self.fail_fast, + #[cfg(feature = "tracing")] + logs_collector: Arc::clone(&self.logs_collector), } } } @@ -437,6 +454,8 @@ impl Default for Basic { before_hook: None, after_hook: None, fail_fast: false, + #[cfg(feature = "tracing")] + logs_collector: Arc::new(AtomicCell::new(Box::new(None))), } } } @@ -530,6 +549,8 @@ impl Basic { before_hook, after_hook, fail_fast, + #[cfg(feature = "tracing")] + logs_collector, .. } = self; Basic { @@ -543,6 +564,8 @@ impl Basic { before_hook, after_hook, fail_fast, + #[cfg(feature = "tracing")] + logs_collector, } } @@ -592,6 +615,8 @@ impl Basic { retry_options, after_hook, fail_fast, + #[cfg(feature = "tracing")] + logs_collector, .. } = self; Basic { @@ -605,6 +630,8 @@ impl Basic { before_hook: Some(func), after_hook, fail_fast, + #[cfg(feature = "tracing")] + logs_collector, } } @@ -641,6 +668,8 @@ impl Basic { retry_options, before_hook, fail_fast, + #[cfg(feature = "tracing")] + logs_collector, .. } = self; Basic { @@ -654,6 +683,8 @@ impl Basic { before_hook, after_hook: Some(func), fail_fast, + #[cfg(feature = "tracing")] + logs_collector, } } @@ -729,6 +760,8 @@ where where S: Stream> + 'static, { + #[cfg(feature = "tracing")] + let logs_collector = *self.logs_collector.swap(Box::new(None)); let Self { max_concurrent_scenarios, retries, @@ -740,6 +773,7 @@ where before_hook, after_hook, fail_fast, + .. } = self; cli.retry = cli.retry.or(retries); @@ -768,6 +802,8 @@ where before_hook, after_hook, fail_fast, + #[cfg(feature = "tracing")] + logs_collector, ); stream::select( @@ -859,6 +895,7 @@ async fn insert_features( /// [`Feature`]: gherkin::Feature /// [`Rule`]: gherkin::Rule /// [`Scenario`]: gherkin::Scenario +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] async fn execute( features: Features, max_concurrent_scenarios: Option, @@ -869,6 +906,7 @@ async fn execute( before_hook: Option, after_hook: Option, fail_fast: bool, + #[cfg(feature = "tracing")] mut logs_collector: Option, ) where W: World, Before: 'static @@ -918,16 +956,14 @@ async fn execute( ControlFlow::Break(()) => Some(0), }; + #[cfg(feature = "tracing")] + let waiter = logs_collector + .as_ref() + .map(TracingCollector::scenario_span_event_waiter); + let mut started_scenarios = ControlFlow::Continue(max_concurrent_scenarios); let mut run_scenarios = stream::FuturesUnordered::new(); loop { - // We yield once on every iteration, because there is a chance, that - // this function never yields otherwise. In this case event sender won't - // send anything to the `Writer` until the end. This is the case, when - // all the parsing is done, so there is no contention on the `Mutex` - // inside `Features` storage and all `Step` functions don't yield. - yield_now().await; - let (runnable, sleep) = features.get(map_break(started_scenarios)).await; if run_scenarios.is_empty() && runnable.is_empty() { @@ -956,21 +992,63 @@ async fn execute( let started = storage.start_scenarios(&runnable); executor.send_all_events(started); - if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios { - *sc -= runnable.len(); - } - - for (f, r, s, ty, retries) in runnable { - run_scenarios.push(executor.run_scenario(f, r, s, ty, retries)); - } + { + #[cfg(feature = "tracing")] + let forward_logs = { + if let Some(coll) = logs_collector.as_mut() { + coll.start_scenarios(&runnable); + } + async { + loop { + while let Some(logs) = logs_collector + .as_mut() + .and_then(TracingCollector::emitted_logs) + { + executor.send_all_events(logs); + } + future::ready(()).then_yield().await; + } + } + }; + #[cfg(feature = "tracing")] + pin_mut!(forward_logs); + #[cfg(not(feature = "tracing"))] + let forward_logs = future::pending(); - if run_scenarios.next().await.is_some() { if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios { - *sc += 1; + *sc -= runnable.len(); + } + + for (id, f, r, s, ty, retries) in runnable { + run_scenarios.push( + executor + .run_scenario( + id, + f, + r, + s, + ty, + retries, + #[cfg(feature = "tracing")] + waiter.as_ref(), + ) + .then_yield(), + ); + } + + let (finished_scenario, _) = + select_with_biased_first(forward_logs, run_scenarios.next()) + .await + .factor_first(); + if finished_scenario.is_some() { + if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios + { + *sc += 1; + } } } - while let Ok(Some((feat, rule, scenario_failed, retried))) = + while let Ok(Some((id, feat, rule, scenario_failed, retried))) = storage.finished_receiver.try_next() { if let Some(rule) = rule { @@ -985,6 +1063,14 @@ async fn execute( if let Some(f) = storage.feature_scenario_finished(feat, retried) { executor.send_event(f); } + #[cfg(feature = "tracing")] + { + if let Some(coll) = logs_collector.as_mut() { + coll.finish_scenario(id); + } + } + #[cfg(not(feature = "tracing"))] + let _ = id; if fail_fast && scenario_failed && !retried { started_scenarios = ControlFlow::Break(()); @@ -1090,14 +1176,16 @@ where /// [`Feature`]: gherkin::Feature /// [`Rule`]: gherkin::Rule /// [`Scenario`]: gherkin::Scenario - #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_arguments, clippy::too_many_lines)] async fn run_scenario( &self, + id: ScenarioId, feature: Arc, rule: Option>, scenario: Arc, scenario_ty: ScenarioType, retries: Option, + #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>, ) { let retry_num = retries.map(|r| r.retries); let ok = |e: fn(_) -> event::Scenario| { @@ -1138,106 +1226,163 @@ where event::Scenario::Started.with_retries(retry_num), )); - let mut result = async { - let before_hook = self - .run_before_hook(&feature, rule.as_ref(), &scenario, retry_num) - .await?; - - let feature_background = feature - .background - .as_ref() - .map(|b| b.steps.iter().map(|s| Arc::new(s.clone()))) - .into_iter() - .flatten(); - - let feature_background = stream::iter(feature_background) - .map(Ok) - .try_fold(before_hook, |world, bg_step| { - self.run_step(world, bg_step, true, into_bg_step_ev) + let is_failed = async { + let mut result = async { + let before_hook = self + .run_before_hook( + &feature, + rule.as_ref(), + &scenario, + retry_num, + id, + #[cfg(feature = "tracing")] + waiter, + ) + .await?; + + let feature_background = feature + .background + .as_ref() + .map(|b| b.steps.iter().map(|s| Arc::new(s.clone()))) + .into_iter() + .flatten(); + + let feature_background = stream::iter(feature_background) + .map(Ok) + .try_fold(before_hook, |world, bg_step| { + self.run_step( + world, + bg_step, + true, + into_bg_step_ev, + id, + #[cfg(feature = "tracing")] + waiter, + ) .map_ok(Some) - }) - .await?; - - let rule_background = rule - .as_ref() - .map(|r| { - r.background - .as_ref() - .map(|b| b.steps.iter().map(|s| Arc::new(s.clone()))) - .into_iter() - .flatten() - }) - .into_iter() - .flatten(); + }) + .await?; + + let rule_background = rule + .as_ref() + .map(|r| { + r.background + .as_ref() + .map(|b| { + b.steps.iter().map(|s| Arc::new(s.clone())) + }) + .into_iter() + .flatten() + }) + .into_iter() + .flatten(); - let rule_background = stream::iter(rule_background) - .map(Ok) - .try_fold(feature_background, |world, bg_step| { - self.run_step(world, bg_step, true, into_bg_step_ev) + let rule_background = stream::iter(rule_background) + .map(Ok) + .try_fold(feature_background, |world, bg_step| { + self.run_step( + world, + bg_step, + true, + into_bg_step_ev, + id, + #[cfg(feature = "tracing")] + waiter, + ) .map_ok(Some) - }) - .await?; + }) + .await?; - stream::iter(scenario.steps.iter().map(|s| Arc::new(s.clone()))) - .map(Ok) - .try_fold(rule_background, |world, step| { - self.run_step(world, step, false, into_step_ev).map_ok(Some) - }) + stream::iter(scenario.steps.iter().map(|s| Arc::new(s.clone()))) + .map(Ok) + .try_fold(rule_background, |world, step| { + self.run_step( + world, + step, + false, + into_step_ev, + id, + #[cfg(feature = "tracing")] + waiter, + ) + .map_ok(Some) + }) + .await + } + .await; + + let (world, scenario_finished_ev) = match &mut result { + Ok(world) => { + (world.take(), event::ScenarioFinished::StepPassed) + } + Err(exec_err) => ( + exec_err.take_world(), + exec_err.get_scenario_finished_event(), + ), + }; + + let (world, after_hook_meta, after_hook_error) = self + .run_after_hook( + world, + &feature, + rule.as_ref(), + &scenario, + scenario_finished_ev, + id, + #[cfg(feature = "tracing")] + waiter, + ) .await - } - .await; + .map_or_else( + |(w, meta, info)| (w.map(Arc::new), Some(meta), Some(info)), + |(w, meta)| (w.map(Arc::new), meta, None), + ); - let (world, scenario_finished_ev) = match &mut result { - Ok(world) => (world.take(), event::ScenarioFinished::StepPassed), - Err(exec_err) => ( - exec_err.take_world(), - exec_err.get_scenario_finished_event(), - ), - }; + let scenario_failed = match &result { + Ok(_) | Err(ExecutionFailure::StepSkipped(_)) => false, + Err( + ExecutionFailure::BeforeHookPanicked { .. } + | ExecutionFailure::StepPanicked { .. }, + ) => true, + }; + let is_failed = scenario_failed || after_hook_error.is_some(); - let (world, after_hook_meta, after_hook_error) = self - .run_after_hook( - world, - &feature, - rule.as_ref(), - &scenario, - scenario_finished_ev, - ) - .await - .map_or_else( - |(w, meta, info)| (w.map(Arc::new), Some(meta), Some(info)), - |(w, meta)| (w.map(Arc::new), meta, None), - ); - - let scenario_failed = match &result { - Ok(_) | Err(ExecutionFailure::StepSkipped(_)) => false, - Err( - ExecutionFailure::BeforeHookPanicked { .. } - | ExecutionFailure::StepPanicked { .. }, - ) => true, - }; - let is_failed = scenario_failed || after_hook_error.is_some(); + if let Some(exec_error) = result.err() { + self.emit_failed_events( + Arc::clone(&feature), + rule.clone(), + Arc::clone(&scenario), + world.clone(), + exec_error, + retry_num, + ); + } - if let Some(exec_error) = result.err() { - self.emit_failed_events( + self.emit_after_hook_events( Arc::clone(&feature), rule.clone(), Arc::clone(&scenario), - world.clone(), - exec_error, + world, + after_hook_meta, + after_hook_error, retry_num, ); - } - self.emit_after_hook_events( - Arc::clone(&feature), - rule.clone(), - Arc::clone(&scenario), - world, - after_hook_meta, - after_hook_error, - retry_num, - ); + is_failed + }; + #[cfg(feature = "tracing")] + let (is_failed, span_id) = { + let span = id.scenario_span(); + let span_id = span.id(); + let is_failed = tracing::Instrument::instrument(is_failed, span); + (is_failed, span_id) + }; + let is_failed = is_failed.then_yield().await; + + #[cfg(feature = "tracing")] + if let Some((waiter, span_id)) = waiter.zip(span_id) { + waiter.wait_for_span_close(span_id).then_yield().await; + } self.send_event(event::Cucumber::scenario( Arc::clone(&feature), @@ -1261,7 +1406,13 @@ where .await; } - self.scenario_finished(feature, rule, is_failed, next_try.is_some()); + self.scenario_finished( + id, + feature, + rule, + is_failed, + next_try.is_some(), + ); } /// Executes [`HookType::Before`], if present. @@ -1278,10 +1429,13 @@ where rule: Option<&Arc>, scenario: &Arc, retries: Option, + scenario_id: ScenarioId, + #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>, ) -> Result, ExecutionFailure> { let init_world = async { AssertUnwindSafe(async { W::new().await }) .catch_unwind() + .then_yield() .await .map_err(Info::from) .and_then(|r| { @@ -1319,7 +1473,24 @@ where } }); - match fut.await { + #[cfg(feature = "tracing")] + let (fut, span_id) = { + let span = scenario_id.hook_span(HookType::Before); + let span_id = span.id(); + let fut = tracing::Instrument::instrument(fut, span); + (fut, span_id) + }; + #[cfg(not(feature = "tracing"))] + let _ = scenario_id; + + let result = fut.then_yield().await; + + #[cfg(feature = "tracing")] + if let Some((waiter, id)) = waiter.zip(span_id) { + waiter.wait_for_span_close(id).then_yield().await; + } + + match result { Ok(world) => { self.send_event(event::Cucumber::scenario( Arc::clone(feature), @@ -1358,6 +1529,8 @@ where step: Arc, is_background: bool, (started, passed, skipped): (St, Ps, Sk), + scenario_id: ScenarioId, + #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>, ) -> Result> where St: FnOnce(Arc) -> event::Cucumber, @@ -1386,6 +1559,7 @@ where } else { match AssertUnwindSafe(async { W::new().await }) .catch_unwind() + .then_yield() .await { Ok(Ok(w)) => w, @@ -1414,7 +1588,23 @@ where } }; - match run.await { + #[cfg(feature = "tracing")] + let (run, span_id) = { + let span = scenario_id.step_span(is_background); + let span_id = span.id(); + let run = tracing::Instrument::instrument(run, span); + (run, span_id) + }; + let result = run.then_yield().await; + + #[cfg(feature = "tracing")] + if let Some((waiter, id)) = waiter.zip(span_id) { + waiter.wait_for_span_close(id).then_yield().await; + } + #[cfg(not(feature = "tracing"))] + let _ = scenario_id; + + match result { Ok((Some(captures), loc, Some(world))) => { self.send_event(passed(step, captures, loc)); Ok(world) @@ -1531,6 +1721,7 @@ where /// /// Doesn't emit any events, see [`Self::emit_failed_events()`] for more /// details. + #[allow(clippy::too_many_arguments)] async fn run_after_hook( &self, mut world: Option, @@ -1538,6 +1729,8 @@ where rule: Option<&Arc>, scenario: &Arc, ev: event::ScenarioFinished, + scenario_id: ScenarioId, + #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>, ) -> Result< (Option, Option), (Option, AfterHookEventsMeta, Info), @@ -1555,7 +1748,25 @@ where }; let started = event::Metadata::new(()); - let res = AssertUnwindSafe(fut).catch_unwind().await; + let fut = AssertUnwindSafe(fut).catch_unwind(); + + #[cfg(feature = "tracing")] + let (fut, span_id) = { + let span = scenario_id.hook_span(HookType::After); + let span_id = span.id(); + let fut = tracing::Instrument::instrument(fut, span); + (fut, span_id) + }; + #[cfg(not(feature = "tracing"))] + let _ = scenario_id; + + let res = fut.then_yield().await; + + #[cfg(feature = "tracing")] + if let Some((waiter, id)) = waiter.zip(span_id) { + waiter.wait_for_span_close(id).then_yield().await; + } + let finished = event::Metadata::new(()); let meta = AfterHookEventsMeta { started, finished }; @@ -1624,6 +1835,7 @@ where /// [`Scenario`]: gherkin::Scenario fn scenario_finished( &self, + id: ScenarioId, feature: Arc, rule: Option>, is_failed: IsFailed, @@ -1633,7 +1845,7 @@ where // so we can just ignore it. drop( self.finished_sender - .unbounded_send((feature, rule, is_failed, is_retried)), + .unbounded_send((id, feature, rule, is_failed, is_retried)), ); } @@ -1665,7 +1877,7 @@ where /// [`Cucumber`]: event::Cucumber fn send_all_events( &self, - events: impl Iterator>, + events: impl IntoIterator>, ) { for v in events { // If the receiver end is dropped, then no one listens for events, @@ -1677,6 +1889,30 @@ where } } +/// ID of a [`Scenario`], uniquely identifying it. +/// +/// **NOTE**: Retried [`Scenario`] has a different ID from a failed one. +/// +/// [`Scenario`]: gherkin::Scenario +#[derive(Clone, Copy, Debug, Display, Eq, FromStr, Hash, PartialEq)] +pub struct ScenarioId(pub(crate) u64); + +impl ScenarioId { + /// Creates a new unique [`ScenarioId`]. + pub fn new() -> Self { + /// [`AtomicU64`] ID. + static ID: AtomicU64 = AtomicU64::new(0); + + Self(ID.fetch_add(1, Ordering::Relaxed)) + } +} + +impl Default for ScenarioId { + fn default() -> Self { + Self::new() + } +} + /// Stores currently running [`Rule`]s and [`Feature`]s and notifies about their /// state of completion. /// @@ -1711,6 +1947,7 @@ struct FinishedRulesAndFeatures { /// /// [`Feature`]: gherkin::Feature type FinishedFeaturesSender = mpsc::UnboundedSender<( + ScenarioId, Arc, Option>, IsFailed, @@ -1722,6 +1959,7 @@ type FinishedFeaturesSender = mpsc::UnboundedSender<( /// /// [`Feature`]: gherkin::Feature type FinishedFeaturesReceiver = mpsc::UnboundedReceiver<( + ScenarioId, Arc, Option>, IsFailed, @@ -1825,6 +2063,7 @@ impl FinishedRulesAndFeatures { &mut self, runnable: impl AsRef< [( + ScenarioId, Arc, Option>, Arc, @@ -1836,7 +2075,7 @@ impl FinishedRulesAndFeatures { let runnable = runnable.as_ref(); let mut started_features = Vec::new(); - for feature in runnable.iter().map(|(f, ..)| Arc::clone(f)).dedup() { + for feature in runnable.iter().map(|(_, f, ..)| Arc::clone(f)).dedup() { let _ = self .features_scenarios_count .entry(Arc::clone(&feature)) @@ -1849,7 +2088,7 @@ impl FinishedRulesAndFeatures { let mut started_rules = Vec::new(); for (feat, rule) in runnable .iter() - .filter_map(|(feat, rule, _, _, _)| { + .filter_map(|(_, feat, rule, _, _, _)| { rule.clone().map(|r| (Arc::clone(feat), r)) }) .dedup() @@ -1880,6 +2119,7 @@ impl FinishedRulesAndFeatures { type Scenarios = HashMap< ScenarioType, Vec<( + ScenarioId, Arc, Option>, Arc, @@ -1891,6 +2131,7 @@ type Scenarios = HashMap< type InsertedScenarios = HashMap< ScenarioType, Vec<( + ScenarioId, Arc, Option>, Arc, @@ -1946,13 +2187,14 @@ impl Features { .map(|(feat, rule, scenario)| { let retries = retry(feat, rule, scenario, cli); ( + ScenarioId::new(), Arc::new(feat.clone()), rule.map(|r| Arc::new(r.clone())), Arc::new(scenario.clone()), retries, ) }) - .into_group_map_by(|(f, r, s, _)| { + .into_group_map_by(|(_, f, r, s, _)| { which_scenario(f, r.as_ref().map(AsRef::as_ref), s) }); @@ -1972,8 +2214,11 @@ impl Features { retries: Option, ) { self.insert_scenarios( - iter::once((scenario_ty, vec![(feature, rule, scenario, retries)])) - .collect(), + iter::once(( + scenario_ty, + vec![(ScenarioId::new(), feature, rule, scenario, retries)], + )) + .collect(), ) .await; } @@ -1987,7 +2232,7 @@ impl Features { let mut with_retries = HashMap::<_, Vec<_>>::new(); let mut without_retries: Scenarios = HashMap::new(); for (which, values) in scenarios { - for (f, r, s, ret) in values { + for (id, f, r, s, ret) in values { match ret { ret @ (None | Some(RetryOptions { @@ -2000,14 +2245,14 @@ impl Features { without_retries .entry(which) .or_default() - .push((f, r, s, ret)); + .push((id, f, r, s, ret)); } Some(ret) => { let ret = ret.with_deadline(now); with_retries .entry(which) .or_default() - .push((f, r, s, ret)); + .push((id, f, r, s, ret)); } } } @@ -2017,8 +2262,8 @@ impl Features { for (which, values) in with_retries { let ty_storage = storage.entry(which).or_default(); - for (f, r, s, ret) in values { - ty_storage.insert(0, (f, r, s, Some(ret))); + for (id, f, r, s, ret) in values { + ty_storage.insert(0, (id, f, r, s, Some(ret))); } } @@ -2050,6 +2295,7 @@ impl Features { max_concurrent_scenarios: Option, ) -> ( Vec<( + ScenarioId, Arc, Option>, Arc, @@ -2058,6 +2304,7 @@ impl Features { )>, Option, ) { + use RetryOptionsWithDeadline as WithDeadline; use ScenarioType::{Concurrent, Serial}; if max_concurrent_scenarios == Some(0) { @@ -2066,14 +2313,14 @@ impl Features { let mut min_dur = None; let mut drain = - |storage: &mut Vec<(_, _, _, Option)>, + |storage: &mut Vec<(_, _, _, _, Option)>, ty, count: Option| { let mut i = 0; // TODO: Replace with `drain_filter`, once stabilized. // https://github.com/rust-lang/rust/issues/43244 let drained = - VecExt::drain_filter(storage, |(_, _, _, ret)| { + VecExt::drain_filter(storage, |(_, _, _, _, ret)| { // Because `drain_filter` runs over entire `Vec` on // `Drop`, we can't just `.take(count)`. if count.filter(|c| i >= *c).is_some() { @@ -2081,9 +2328,7 @@ impl Features { } ret.as_ref() - .and_then( - RetryOptionsWithDeadline::left_until_retry, - ) + .and_then(WithDeadline::left_until_retry) .map_or_else( || { i += 1; @@ -2097,7 +2342,9 @@ impl Features { }, ) }) - .map(|(f, r, s, ret)| (f, r, s, ty, ret.map(Into::into))) + .map(|(id, f, r, s, ret)| { + (id, f, r, s, ty, ret.map(Into::into)) + }) .collect::>(); (!drained.is_empty()).then_some(drained) }; diff --git a/src/tracing.rs b/src/tracing.rs new file mode 100644 index 00000000..7eee99e5 --- /dev/null +++ b/src/tracing.rs @@ -0,0 +1,622 @@ +//! [`tracing`] integration layer. + +use std::{collections::HashMap, fmt, io, iter, sync::Arc}; + +use futures::channel::{mpsc, oneshot}; +use itertools::Either; +use tracing::{ + field::{Field, Visit}, + span, Dispatch, Event, Span, Subscriber, +}; +use tracing_subscriber::{ + field::RecordFields, + filter::LevelFilter, + fmt::{ + format::{self, Format}, + FmtContext, FormatEvent, FormatFields, MakeWriter, + }, + layer::{self, Layer, Layered, SubscriberExt as _}, + registry::LookupSpan, + util::SubscriberInitExt as _, +}; + +use crate::{ + event::{self, HookType}, + runner::{ + self, + basic::{RetryOptions, ScenarioId}, + }, + Cucumber, Parser, Runner, ScenarioType, World, Writer, +}; + +impl + Cucumber, Wr, Cli> +where + W: World, + P: Parser, + runner::Basic: Runner, + Wr: Writer, + Cli: clap::Args, +{ + /// Initializes a global [`tracing::Subscriber`] with a default + /// [`fmt::Layer`] and [`LevelFilter::INFO`]. + /// + /// [`fmt::Layer`]: tracing_subscriber::fmt::Layer + #[must_use] + pub fn init_tracing(self) -> Self { + self.configure_and_init_tracing( + format::DefaultFields::new(), + Format::default(), + |layer| { + tracing_subscriber::registry() + .with(LevelFilter::INFO.and_then(layer)) + }, + ) + } + + /// Configures a [`fmt::Layer`], additionally wraps it (for example, into a + /// [`LevelFilter`]), and initializes as a global [`tracing::Subscriber`]. + /// + /// # Example + /// + /// ```rust + /// # use cucumber::{Cucumber, World as _}; + /// # use tracing_subscriber::{ + /// # filter::LevelFilter, + /// # fmt::format::{self, Format}, + /// # layer::SubscriberExt, + /// # Layer, + /// # }; + /// # + /// # #[derive(Debug, Default, cucumber::World)] + /// # struct World; + /// # + /// # let _ = async { + /// World::cucumber() + /// .configure_and_init_tracing( + /// format::DefaultFields::new(), + /// Format::default(), + /// |fmt_layer| { + /// tracing_subscriber::registry() + /// .with(LevelFilter::INFO.and_then(fmt_layer)) + /// }, + /// ) + /// .run_and_exit("./tests/features/doctests.feature") + /// .await + /// # }; + /// ``` + /// + /// [`fmt::Layer`]: tracing_subscriber::fmt::Layer + #[must_use] + pub fn configure_and_init_tracing( + self, + fmt_fields: Fields, + event_format: Event, + configure: Conf, + ) -> Self + where + Fields: for<'a> FormatFields<'a> + 'static, + Event: FormatEvent> + 'static, + Sub: Subscriber + for<'a> LookupSpan<'a>, + Out: Subscriber + Send + Sync, + // TODO: Replace the inner type with TAIT, once stabilized: + // https://github.com/rust-lang/rust/issues/63063 + Conf: FnOnce( + Layered< + tracing_subscriber::fmt::Layer< + Sub, + SkipScenarioIdSpan, + AppendScenarioMsg, + CollectorWriter, + >, + RecordScenarioId, + Sub, + >, + ) -> Out, + { + let (logs_sender, logs_receiver) = mpsc::unbounded(); + let (span_close_sender, span_close_receiver) = mpsc::unbounded(); + + let layer = RecordScenarioId::new(span_close_sender).and_then( + tracing_subscriber::fmt::layer() + .fmt_fields(SkipScenarioIdSpan(fmt_fields)) + .event_format(AppendScenarioMsg(event_format)) + .with_writer(CollectorWriter::new(logs_sender)), + ); + Dispatch::new(configure(layer)).init(); + + drop( + self.runner + .logs_collector + .swap(Box::new(Some(Collector::new( + logs_receiver, + span_close_receiver, + )))), + ); + + self + } +} + +/// [`HashMap`] from a [`ScenarioId`] to its [`Scenario`] and full path. +/// +/// [`Scenario`]: gherkin::Scenario +type Scenarios = HashMap< + ScenarioId, + ( + Arc, + Option>, + Arc, + Option, + ), +>; + +/// All [`Callback`]s for [`Span`]s closing events with their completion status. +type SpanEventsCallbacks = + HashMap>, IsReceived)>; + +/// Indication whether a [`Span`] closing event was received. +type IsReceived = bool; + +/// Callback for notifying a [`Runner`] about a [`Span`] being closed. +type Callback = oneshot::Sender<()>; + +/// Collector of [`tracing::Event`]s. +#[derive(Debug)] +pub(crate) struct Collector { + /// [`Scenarios`] with their IDs. + scenarios: Scenarios, + + /// Receiver of [`tracing::Event`]s messages with optional corresponding + /// [`ScenarioId`]. + logs_receiver: mpsc::UnboundedReceiver<(Option, String)>, + + /// All [`Callback`]s for [`Span`]s closing events with their completion + /// status. + span_events: SpanEventsCallbacks, + + /// Receiver of a [`Span`] closing event. + span_close_receiver: mpsc::UnboundedReceiver, + + /// Sender for subscribing to a [`Span`] closing event. + wait_span_event_sender: mpsc::UnboundedSender<(span::Id, Callback)>, + + /// Receiver for subscribing to a [`Span`] closing event. + wait_span_event_receiver: mpsc::UnboundedReceiver<(span::Id, Callback)>, +} + +impl Collector { + /// Creates a new [`tracing::Event`]s [`Collector`]. + pub(crate) fn new( + logs_receiver: mpsc::UnboundedReceiver<(Option, String)>, + span_close_receiver: mpsc::UnboundedReceiver, + ) -> Self { + let (sender, receiver) = mpsc::unbounded(); + Self { + scenarios: HashMap::new(), + logs_receiver, + span_events: HashMap::new(), + span_close_receiver, + wait_span_event_sender: sender, + wait_span_event_receiver: receiver, + } + } + + /// Creates a new [`SpanCloseWaiter`]. + pub(crate) fn scenario_span_event_waiter(&self) -> SpanCloseWaiter { + SpanCloseWaiter { + wait_span_event_sender: self.wait_span_event_sender.clone(), + } + } + + /// Starts [`Scenario`]s from the provided `runnable`. + /// + /// [`Scenario`]: gherkin::Scenario + pub(crate) fn start_scenarios( + &mut self, + runnable: impl AsRef< + [( + ScenarioId, + Arc, + Option>, + Arc, + ScenarioType, + Option, + )], + >, + ) { + for (id, f, r, s, _, ret) in runnable.as_ref() { + drop(self.scenarios.insert( + *id, + ( + Arc::clone(f), + r.as_ref().map(Arc::clone), + Arc::clone(s), + *ret, + ), + )); + } + } + + /// Marks a [`Scenario`] as finished, by its ID. + /// + /// [`Scenario`]: gherkin::Scenario + pub(crate) fn finish_scenario(&mut self, id: ScenarioId) { + drop(self.scenarios.remove(&id)); + } + + /// Returns all the emitted [`event::Scenario::Log`]s since this method was + /// last called. + /// + /// In case a received [`tracing::Event`] doesn't contain a [`Scenario`]'s + /// [`Span`], such [`tracing::Event`] will be forwarded to all active + /// [`Scenario`]s. + /// + /// [`Scenario`]: gherkin::Scenario + pub(crate) fn emitted_logs( + &mut self, + ) -> Option>> { + self.notify_about_closing_spans(); + + self.logs_receiver + .try_next() + .ok() + .flatten() + .map(|(id, msg)| { + id.and_then(|k| self.scenarios.get(&k)) + .map_or_else( + || Either::Left(self.scenarios.values()), + |p| Either::Right(iter::once(p)), + ) + .map(|(f, r, s, opt)| { + event::Cucumber::scenario( + Arc::clone(f), + r.as_ref().map(Arc::clone), + Arc::clone(s), + event::RetryableScenario { + event: event::Scenario::Log(msg.clone()), + retries: opt.map(|o| o.retries), + }, + ) + }) + .collect() + }) + } + + /// Notifies all its subscribers about closing [`Span`]s via [`Callback`]s. + fn notify_about_closing_spans(&mut self) { + if let Some(id) = self.span_close_receiver.try_next().ok().flatten() { + self.span_events.entry(id).or_default().1 = true; + } + while let Some((id, callback)) = + self.wait_span_event_receiver.try_next().ok().flatten() + { + self.span_events + .entry(id) + .or_default() + .0 + .get_or_insert(Vec::new()) + .push(callback); + } + self.span_events.retain(|_, (callbacks, is_received)| { + if callbacks.is_some() && *is_received { + for callback in callbacks + .take() + .unwrap_or_else(|| unreachable!("`callbacks.is_some()`")) + { + let _ = callback.send(()).ok(); + } + false + } else { + true + } + }); + } +} + +// We better keep this here, as it's related to `tracing` capabilities only. +#[allow(clippy::multiple_inherent_impl)] +impl ScenarioId { + /// Name of the [`ScenarioId`] [`Span`] field. + const SPAN_FIELD_NAME: &'static str = "__cucumber_scenario_id"; + + /// Creates a new [`Span`] for running a [`Scenario`] with this + /// [`ScenarioId`]. + /// + /// [`Scenario`]: gherkin::Scenario + pub(crate) fn scenario_span(self) -> Span { + // `Level::ERROR` is used to minimize the chance of the user-provided + // filter to skip it. + tracing::error_span!("scenario", __cucumber_scenario_id = self.0) + } + + /// Creates a new [`Span`] for a running [`Step`]. + /// + /// [`Step`]: gherkin::Step + #[allow(clippy::unused_self)] + pub(crate) fn step_span(self, is_background: bool) -> Span { + // `Level::ERROR` is used to minimize the chance of the user-provided + // filter to skip it. + if is_background { + tracing::error_span!("background step") + } else { + tracing::error_span!("step") + } + } + + /// Creates a new [`Span`] for running a [`Hook`]. + /// + /// [`Hook`]: event::Hook + #[allow(clippy::unused_self)] + pub(crate) fn hook_span(self, hook_ty: HookType) -> Span { + // `Level::ERROR` is used to minimize the chance of the user-provided + // filter to skip it. + match hook_ty { + HookType::Before => tracing::error_span!("before hook"), + HookType::After => tracing::error_span!("after hook"), + } + } +} + +/// Waiter for a particular [`Span`] to be closed, wich is required because a +/// [`CollectorWriter`] can notify about an [`event::Scenario::Log`] after a +/// [`Scenario`]/[`Step`] is considered [`Finished`] already, due to +/// implementation details of a [`Subscriber`]. +/// +/// [`Finished`]: event::Scenario::Finished +/// [`Scenario`]: gherkin::Scenario +/// [`Step`]: gherkin::Step +#[derive(Clone, Debug)] +pub(crate) struct SpanCloseWaiter { + /// Sender for subscribing to the [`Span`] closing. + wait_span_event_sender: mpsc::UnboundedSender<(span::Id, Callback)>, +} + +impl SpanCloseWaiter { + /// Waits for the [`Span`] being closed. + pub(crate) async fn wait_for_span_close(&self, id: span::Id) { + let (sender, receiver) = oneshot::channel(); + let _ = self + .wait_span_event_sender + .unbounded_send((id, sender)) + .ok(); + let _ = receiver.await.ok(); + } +} + +/// [`Layer`] recording a [`ScenarioId`] into [`Span`]'s [`Extensions`]. +/// +/// [`Extensions`]: tracing_subscriber::registry::Extensions +#[derive(Debug)] +pub struct RecordScenarioId { + /// Sender for [`Span`] closing events. + span_close_sender: mpsc::UnboundedSender, +} + +impl RecordScenarioId { + /// Creates a new [`RecordScenarioId`] [`Layer`]. + const fn new(span_close_sender: mpsc::UnboundedSender) -> Self { + Self { span_close_sender } + } +} + +impl Layer for RecordScenarioId +where + S: for<'a> LookupSpan<'a> + Subscriber, +{ + fn on_new_span( + &self, + attr: &span::Attributes<'_>, + id: &span::Id, + ctx: layer::Context<'_, S>, + ) { + if let Some(span) = ctx.span(id) { + let mut visitor = GetScenarioId(None); + attr.values().record(&mut visitor); + + if let Some(scenario_id) = visitor.0 { + let mut ext = span.extensions_mut(); + let _ = ext.replace(scenario_id); + } + } + } + + fn on_record( + &self, + id: &span::Id, + values: &span::Record<'_>, + ctx: layer::Context<'_, S>, + ) { + if let Some(span) = ctx.span(id) { + let mut visitor = GetScenarioId(None); + values.record(&mut visitor); + + if let Some(scenario_id) = visitor.0 { + let mut ext = span.extensions_mut(); + let _ = ext.replace(scenario_id); + } + } + } + + fn on_close(&self, id: span::Id, _ctx: layer::Context<'_, S>) { + let _ = self.span_close_sender.unbounded_send(id).ok(); + } +} + +/// [`Visit`]or extracting a [`ScenarioId`] from a +/// [`ScenarioId::SPAN_FIELD_NAME`]d [`Field`], in case it's present. +#[derive(Debug)] +struct GetScenarioId(Option); + +impl Visit for GetScenarioId { + fn record_u64(&mut self, field: &Field, value: u64) { + if field.name() == ScenarioId::SPAN_FIELD_NAME { + self.0 = Some(ScenarioId(value)); + } + } + + fn record_debug(&mut self, _: &Field, _: &dyn fmt::Debug) {} +} + +/// [`FormatFields`] wrapper skipping [`Span`]s with a [`ScenarioId`]. +#[derive(Debug)] +pub struct SkipScenarioIdSpan(pub F); + +impl<'w, F: FormatFields<'w>> FormatFields<'w> for SkipScenarioIdSpan { + fn format_fields( + &self, + writer: format::Writer<'w>, + fields: R, + ) -> fmt::Result { + let mut is_scenario_span = IsScenarioIdSpan(false); + fields.record(&mut is_scenario_span); + if !is_scenario_span.0 { + self.0.format_fields(writer, fields)?; + } + Ok(()) + } +} + +/// [`Visit`]or checking whether a [`Span`] has a [`Field`] with the +/// [`ScenarioId::SPAN_FIELD_NAME`]. +#[derive(Debug)] +struct IsScenarioIdSpan(bool); + +impl Visit for IsScenarioIdSpan { + fn record_debug(&mut self, field: &Field, _: &dyn fmt::Debug) { + if field.name() == ScenarioId::SPAN_FIELD_NAME { + self.0 = true; + } + } +} + +/// [`FormatEvent`] wrapper, appending [`tracing::Event`]s with some markers, +/// to parse them later and retrieve optional [`ScenarioId`]. +/// +/// [`Scenario`]: gherkin::Scenario +#[derive(Debug)] +pub struct AppendScenarioMsg(pub F); + +impl FormatEvent for AppendScenarioMsg +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, + F: FormatEvent, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: format::Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + self.0.format_event(ctx, writer.by_ref(), event)?; + + if let Some(scenario_id) = ctx.event_scope().and_then(|scope| { + scope + .from_root() + .find_map(|span| span.extensions().get::().copied()) + }) { + writer.write_fmt(format_args!( + "{}{scenario_id}", + suffix::BEFORE_SCENARIO_ID, + ))?; + } else { + writer.write_fmt(format_args!("{}", suffix::NO_SCENARIO_ID))?; + } + writer.write_fmt(format_args!("{}", suffix::END)) + } +} + +mod suffix { + //! [`str`]ings appending [`tracing::Event`]s to separate them later. + //! + //! Every [`tracing::Event`] ends with: + //! + //! ([`BEFORE_SCENARIO_ID`][`ScenarioId`][`END`]|[`NO_SCENARIO_ID`][`END`]) + //! + //! [`ScenarioId`]: super::ScenarioId + + /// End of a [`tracing::Event`] message. + pub(crate) const END: &str = "__cucumber__scenario"; + + /// Separator before a [`ScenarioId`]. + /// + /// [`ScenarioId`]: super::ScenarioId + pub(crate) const BEFORE_SCENARIO_ID: &str = "__"; + + /// Separator in case there is no [`ScenarioId`]. + /// + /// [`ScenarioId`]: super::ScenarioId + pub(crate) const NO_SCENARIO_ID: &str = "__unknown"; +} + +/// [`io::Write`]r sending [`tracing::Event`]s to a `Collector`. +#[derive(Clone, Debug)] +pub struct CollectorWriter { + /// Sender for notifying the [`Collector`] about [`tracing::Event`]s via. + sender: mpsc::UnboundedSender<(Option, String)>, +} + +impl CollectorWriter { + /// Creates a new [`CollectorWriter`]. + const fn new( + sender: mpsc::UnboundedSender<(Option, String)>, + ) -> Self { + Self { sender } + } +} + +impl<'a> MakeWriter<'a> for CollectorWriter { + type Writer = Self; + + fn make_writer(&'a self) -> Self::Writer { + self.clone() + } +} + +impl io::Write for CollectorWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + // Although this is not documented explicitly anywhere, `io::Write`rs + // inside `tracing::fmt::Layer` always receives fully formatted messages + // at once, not by parts. + // Inside docs of `fmt::Layer::with_writer()`, a non-locked `io::stderr` + // is passed as an `io::Writer`. So, if this guarantee fails, parts of + // log messages will be able to interleave each other, making the result + // unreadable. + let msgs = String::from_utf8_lossy(buf); + for msg in msgs.split_terminator(suffix::END) { + if let Some((before, after)) = + msg.rsplit_once(suffix::NO_SCENARIO_ID) + { + if !after.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "wrong separator", + )); + } + let _ = + self.sender.unbounded_send((None, before.to_owned())).ok(); + } else if let Some((before, after)) = + msg.rsplit_once(suffix::BEFORE_SCENARIO_ID) + { + let scenario_id = after.parse().map_err(|e| { + io::Error::new(io::ErrorKind::InvalidData, e) + })?; + let _ = self + .sender + .unbounded_send((Some(scenario_id), before.to_owned())) + .ok(); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "missing separator", + )); + } + } + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/writer/basic.rs b/src/writer/basic.rs index bbfdca17..cf945f6a 100644 --- a/src/writer/basic.rs +++ b/src/writer/basic.rs @@ -123,6 +123,11 @@ pub struct Basic { /// Number of lines to clear. lines_to_clear: usize, + /// Buffer to be re-output after [`clear_last_lines_if_term_present()`][0]. + /// + /// [0]: Self::clear_last_lines_if_term_present + re_output_after_clear: String, + /// [`Verbosity`] of this [`Writer`]. verbosity: Verbosity, } @@ -224,6 +229,7 @@ impl Basic { styles: Styles::new(), indent: 0, lines_to_clear: 0, + re_output_after_clear: String::new(), verbosity: verbosity.into(), }; basic.apply_cli(Cli { @@ -248,6 +254,8 @@ impl Basic { fn clear_last_lines_if_term_present(&mut self) -> io::Result<()> { if self.styles.is_present && self.lines_to_clear > 0 { self.output.clear_last_lines(self.lines_to_clear)?; + self.output.write_str(&self.re_output_after_clear)?; + self.re_output_after_clear.clear(); self.lines_to_clear = 0; } Ok(()) @@ -371,10 +379,18 @@ impl Basic { Scenario::Finished => { self.indent = self.indent.saturating_sub(2); } + Scenario::Log(msg) => self.emit_log(msg)?, } Ok(()) } + /// Outputs the [`event::Scenario::Log`]. + pub(crate) fn emit_log(&mut self, msg: impl AsRef) -> io::Result<()> { + self.lines_to_clear += msg.as_ref().lines().count(); + self.re_output_after_clear.push_str(msg.as_ref()); + self.output.write_str(msg) + } + /// Outputs the [failed] [`Scenario`]'s hook. /// /// [failed]: event::Hook::Failed @@ -534,7 +550,7 @@ impl Basic { .unwrap_or_default(), indent = " ".repeat(self.indent), ); - self.lines_to_clear = output.lines().count(); + self.lines_to_clear += output.lines().count(); self.write_line(&output)?; } Ok(()) @@ -809,7 +825,7 @@ impl Basic { .unwrap_or_default(), indent = " ".repeat(self.indent.saturating_sub(2)), ); - self.lines_to_clear = output.lines().count(); + self.lines_to_clear += output.lines().count(); self.write_line(&output)?; } Ok(()) diff --git a/src/writer/json.rs b/src/writer/json.rs index 341cc8f4..b45cae58 100644 --- a/src/writer/json.rs +++ b/src/writer/json.rs @@ -189,7 +189,8 @@ impl Json { feature, rule, scenario, "scenario", &st, ev, meta, ); } - Scenario::Started | Scenario::Finished => {} + // TODO: Report logs for each `Scenario`. + Scenario::Started | Scenario::Finished | Scenario::Log(_) => {} } } diff --git a/src/writer/junit.rs b/src/writer/junit.rs index 0d6422bc..341c5e6d 100644 --- a/src/writer/junit.rs +++ b/src/writer/junit.rs @@ -311,6 +311,8 @@ impl JUnit { }) .add_testcase(case); } + // TODO: Report logs for each `Scenario`. + Scenario::Log(_) => {} } } @@ -393,6 +395,8 @@ impl JUnit { sc.name, ); } + // TODO: Report logs for each `Scenario`. + Scenario::Log(_) => unreachable!(), }; // We should be passing normalized events here, diff --git a/src/writer/libtest.rs b/src/writer/libtest.rs index f76b098f..204851e4 100644 --- a/src/writer/libtest.rs +++ b/src/writer/libtest.rs @@ -479,6 +479,16 @@ impl Libtest { Scenario::Step(step, ev) => self.expand_step_event( feature, rule, scenario, &step, ev, retries, false, cli, ), + // We do use `print!()` intentionally here to support `libtest` + // output capturing properly, which can only capture output from + // the standard library’s `print!()` macro. + // This is the same as `tracing_subscriber::fmt::TestWriter` does + // (check its documentation for details). + #[allow(clippy::print_stdout)] + Scenario::Log(msg) => { + print!("{msg}"); + vec![] + } } } diff --git a/src/writer/summarize.rs b/src/writer/summarize.rs index 217264fc..6d7c13ca 100644 --- a/src/writer/summarize.rs +++ b/src/writer/summarize.rs @@ -417,7 +417,8 @@ impl Summarize { let ret = ev.retries; match &ev.event { Scenario::Started - | Scenario::Hook(_, Hook::Passed | Hook::Started) => {} + | Scenario::Hook(_, Hook::Passed | Hook::Started) + | Scenario::Log(_) => {} Scenario::Hook(_, Hook::Failed(..)) => { // - If Scenario's last Step failed and then After Hook failed // too, we don't need to track second failure; diff --git a/tests/after_hook.rs b/tests/after_hook.rs index 65550086..31d1ecc3 100644 --- a/tests/after_hook.rs +++ b/tests/after_hook.rs @@ -53,6 +53,7 @@ async fn fires_each_time() { }) .fail_on_skipped() .with_default_cli() + .max_concurrent_scenarios(1) .run_and_exit("tests/features/wait"); let err = AssertUnwindSafe(res) diff --git a/tests/features/tracing/.feature b/tests/features/tracing/.feature new file mode 100644 index 00000000..1fcb5f82 --- /dev/null +++ b/tests/features/tracing/.feature @@ -0,0 +1,15 @@ +Feature: Basic + + @serial + Scenario: deny skipped + Given step 1 + + Scenario Outline: steps + Given step + + Examples: + | step | + | 2 | + | 3 | + | 4 | + | 5 | diff --git a/tests/features/tracing/correct.stdout b/tests/features/tracing/correct.stdout new file mode 100644 index 00000000..c8e97d97 --- /dev/null +++ b/tests/features/tracing/correct.stdout @@ -0,0 +1,25 @@ +Feature: Basic + Scenario: deny skipped + INFO tracing: not in span: 0 + INFO scenario:step: tracing: in span: 1 + ✔ Given step 1 + Scenario Outline: steps + INFO tracing: not in span: 1 + INFO tracing: not in span: 2 + INFO scenario:step: tracing: in span: 2 + ✔ Given step 2 + Scenario Outline: steps + INFO tracing: not in span: 1 + INFO tracing: not in span: 2 + INFO scenario:step: tracing: in span: 3 + ✔ Given step 3 + Scenario Outline: steps + INFO tracing: not in span: 1 + INFO tracing: not in span: 2 + INFO scenario:step: tracing: in span: 4 + ✔ Given step 4 + Scenario Outline: steps + INFO tracing: not in span: 1 + INFO tracing: not in span: 2 + INFO scenario:step: tracing: in span: 5 + ✔ Given step 5 diff --git a/tests/tracing.rs b/tests/tracing.rs new file mode 100644 index 00000000..c24dd5fb --- /dev/null +++ b/tests/tracing.rs @@ -0,0 +1,78 @@ +use std::{fs, io, panic::AssertUnwindSafe, time::Duration}; + +use cucumber::{given, writer, writer::Coloring, World as _, WriterExt as _}; +use derive_more::Display; +use futures::FutureExt as _; +use regex::Regex; +use tokio::{spawn, time}; +use tracing_subscriber::{ + filter::LevelFilter, + fmt::format::{DefaultFields, Format}, + layer::SubscriberExt as _, + Layer, +}; + +#[tokio::main] +async fn main() { + spawn(async { + let mut id = 0; + loop { + time::sleep(Duration::from_millis(600)).await; + tracing::info!("not in span: {id}"); + id += 1; + } + }); + + let mut out = Vec::::new(); + + let res = World::cucumber() + .with_writer( + writer::Basic::raw(&mut out, Coloring::Never, 0) + .discard_stats_writes() + .tee::( + writer::Basic::raw(io::stdout(), Coloring::Never, 0) + .summarized(), + ) + .normalized(), + ) + .fail_on_skipped() + .with_default_cli() + .configure_and_init_tracing( + DefaultFields::new(), + Format::default().with_ansi(false).without_time(), + |layer| { + tracing_subscriber::registry() + .with(LevelFilter::INFO.and_then(layer)) + }, + ) + .run_and_exit("tests/features/tracing"); + + AssertUnwindSafe(res).catch_unwind().await.unwrap(); + + // Required to strip out non-deterministic parts of output, so we could + // compare them well. + let non_deterministic = Regex::new( + " ([^\"\\n\\s]*)[/\\\\]([A-z1-9-_]*)\\.(feature|rs)(:\\d+:\\d+)?\ + |\\s?\n", + ) + .unwrap(); + + assert_eq!( + non_deterministic + .replace_all(String::from_utf8_lossy(&out).as_ref(), ""), + non_deterministic.replace_all( + &fs::read_to_string("tests/features/tracing/correct.stdout") + .unwrap(), + "", + ), + ); +} + +#[given(regex = "step (\\d+)")] +async fn step(_: &mut World, n: usize) { + time::sleep(Duration::from_secs(1)).await; + tracing::info!("in span: {n:?}"); +} + +#[derive(Clone, cucumber::World, Debug, Default, Display)] +struct World;