mock: improve ergonomics when an ExpectedSpan is needed (#3097)

Many of the methods on `MockCollector` take an `ExpectedSpan`. This
often requires significant boilerplate. For example, to expect that a
span with a specific name enters and then exits, the following code is
needed:

```rust
let span = expect::span().named("span name");

let (collector, handle) = collector::mock()
    .enter(span.clone())
    .exit(span)
    .run_with_handle();
```

In order to make using `tracing-mock` more ergonomic and also more
compact, the `MockCollector` and `MockSubscriber` methods that previous
took an `ExpectedSpan`, are now generic over `Into<ExpectedSpan>`.

There are currently 3 implementations of `From` for `ExpectedSpan` which
allow the following shorthand uses:

`T: Into<String>` - an `ExpectedSpan` will be created that expects to
have a name specified by `T`.

```rust
let (collector, handle) = collector::mock()
    .enter("span name")
    .exit("span name")
    .run_with_handle();
```

`&ExpectedId` - an `ExpectedSpan` will be created that expects to have
the expected Id. A reference is taken and cloned internally because the
caller always needs to use an `ExpectedId` in at least 2 calls to the
mock collector/subscriber.

```rust
let id = expect::id();

let (collector, handle) = collector::mock()
    .new_span(&id)
    .enter(&id)
    .run_with_handle();
```

`&ExpectedSpan` - The expected span is taken by reference and cloned.

```rust
let span = expect::span().named("span name");

let (collector, handle) = collector::mock()
    .enter(&span)
    .exit(&span)
    .run_with_handle();
```

In Rust, taking a reference to an object and immediately cloning it is
an anti-pattern. It is considered better to force the user to clone
outside the API to make the cloning explict.

However, in the case of a testing framework, it seems reasonable to
prefer a more concise API, rather than having it more explicit.

To reduce the size of this PR and to avoid unnecessary churn in other
crates, the tests within the tracing repo which use `tracing-mock` will
not be updated to use the new `Into<ExpectedSpan>` capabilities. The new
API is backwards compatible and those tests can remain as they are.
This commit is contained in:
Hayden Stainsby 2024-10-29 15:38:35 +01:00
parent ad260f419d
commit 7b2cbc631e
3 changed files with 196 additions and 93 deletions

View File

@ -1,5 +1,5 @@
//! An implementation of the [`Layer`] trait which validates that
//! the `tracing` data it recieves matches the expected output for a test.
//! the `tracing` data it receives matches the expected output for a test.
//!
//!
//! The [`MockLayer`] is the central component in these tools. The
@ -7,7 +7,7 @@
//! validated as the code under test is run.
//!
//! ```
//! use tracing_mock::{expect, field, layer};
//! use tracing_mock::{expect, layer};
//! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
//!
//! let (layer, handle) = layer::mock()
@ -40,11 +40,11 @@
//! .named("my_span");
//! let (layer, handle) = layer::mock()
//! // Enter a matching span
//! .enter(span.clone())
//! .enter(&span)
//! // Record an event with message "collect parting message"
//! .event(expect::event().with_fields(expect::message("say hello")))
//! // Exit a matching span
//! .exit(span)
//! .exit(&span)
//! // Expect no further messages to be recorded
//! .only()
//! // Return the layer and handle
@ -75,18 +75,18 @@
//! span before recording an event, the test will fail:
//!
//! ```should_panic
//! use tracing_mock::{expect, field, layer};
//! use tracing_mock::{expect, layer};
//! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
//!
//! let span = expect::span()
//! .named("my_span");
//! let (layer, handle) = layer::mock()
//! // Enter a matching span
//! .enter(span.clone())
//! .enter(&span)
//! // Record an event with message "collect parting message"
//! .event(expect::event().with_fields(expect::message("say hello")))
//! // Exit a matching span
//! .exit(span)
//! .exit(&span)
//! // Expect no further messages to be recorded
//! .only()
//! // Return the subscriber and handle
@ -146,18 +146,18 @@ use std::{
/// # Examples
///
/// ```
/// use tracing_mock::{expect, field, layer};
/// use tracing_mock::{expect, layer};
/// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
///
/// let span = expect::span()
/// .named("my_span");
/// let (layer, handle) = layer::mock()
/// // Enter a matching span
/// .enter(span.clone())
/// .enter(&span)
/// // Record an event with message "collect parting message"
/// .event(expect::event().with_fields(expect::message("say hello")))
/// // Exit a matching span
/// .exit(span)
/// .exit(&span)
/// // Expect no further messages to be recorded
/// .only()
/// // Return the subscriber and handle
@ -414,7 +414,7 @@ impl MockLayerBuilder {
///
/// This function accepts `Into<NewSpan>` instead of
/// [`ExpectedSpan`] directly. [`NewSpan`] can be used to test
/// span fields and the span parent.
/// span fields and the span ancestry.
///
/// The new span doesn't need to be entered for this expectation
/// to succeed.
@ -504,8 +504,8 @@ impl MockLayerBuilder {
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (layer, handle) = layer::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .only()
/// .run_with_handle();
///
@ -532,8 +532,8 @@ impl MockLayerBuilder {
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (layer, handle) = layer::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .only()
/// .run_with_handle();
///
@ -552,8 +552,11 @@ impl MockLayerBuilder {
///
/// [`exit`]: fn@Self::exit
/// [`only`]: fn@Self::only
pub fn enter(mut self, span: ExpectedSpan) -> Self {
self.expected.push_back(Expect::Enter(span));
pub fn enter<S>(mut self, span: S) -> Self
where
S: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::Enter(span.into()));
self
}
@ -581,8 +584,8 @@ impl MockLayerBuilder {
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (layer, handle) = layer::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .only()
/// .run_with_handle();
///
@ -608,8 +611,8 @@ impl MockLayerBuilder {
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (layer, handle) = layer::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .only()
/// .run_with_handle();
///
@ -629,8 +632,11 @@ impl MockLayerBuilder {
/// [`enter`]: fn@Self::enter
/// [`MockHandle::assert_finished`]: fn@crate::subscriber::MockHandle::assert_finished
/// [`Span::enter`]: fn@tracing::Span::enter
pub fn exit(mut self, span: ExpectedSpan) -> Self {
self.expected.push_back(Expect::Exit(span));
pub fn exit<S>(mut self, span: S) -> Self
where
S: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::Exit(span.into()));
self
}

View File

@ -18,8 +18,8 @@
//! .at_level(tracing::Level::INFO);
//!
//! let (subscriber, handle) = subscriber::mock()
//! .enter(span.clone())
//! .exit(span)
//! .enter(&span)
//! .exit(&span)
//! .run_with_handle();
//!
//! tracing::subscriber::with_default(subscriber, || {
@ -30,6 +30,25 @@
//! handle.assert_finished();
//! ```
//!
//! Instead of passing an `ExpectedSpan`, the subscriber methods will also accept
//! anything that implements `Into<String>` which is shorthand for
//! `expect::span().named(name)`.
//!
//! ```
//! use tracing_mock::subscriber;
//!
//! let (subscriber, handle) = subscriber::mock()
//! .enter("interesting_span")
//! .run_with_handle();
//!
//! tracing::subscriber::with_default(subscriber, || {
//! let span = tracing::info_span!("interesting_span");
//! let _guard = span.enter();
//! });
//!
//! handle.assert_finished();
//! ```
//
//! The following example asserts the name, level, parent, and fields of the span:
//!
//! ```
@ -44,10 +63,10 @@
//! .with_ancestry(expect::has_explicit_parent("parent_span"));
//!
//! let (subscriber, handle) = subscriber::mock()
//! .new_span(expect::span().named("parent_span"))
//! .new_span("parent_span")
//! .new_span(new_span)
//! .enter(span.clone())
//! .exit(span)
//! .enter(&span)
//! .exit(&span)
//! .run_with_handle();
//!
//! tracing::subscriber::with_default(subscriber, || {
@ -75,8 +94,8 @@
//! .at_level(tracing::Level::INFO);
//!
//! let (subscriber, handle) = subscriber::mock()
//! .enter(span.clone())
//! .exit(span)
//! .enter(&span)
//! .exit(&span)
//! .run_with_handle();
//!
//! tracing::subscriber::with_default(subscriber, || {
@ -115,6 +134,27 @@ pub struct ExpectedSpan {
pub(crate) metadata: ExpectedMetadata,
}
impl<I> From<I> for ExpectedSpan
where
I: Into<String>,
{
fn from(name: I) -> Self {
ExpectedSpan::default().named(name)
}
}
impl From<&ExpectedId> for ExpectedSpan {
fn from(id: &ExpectedId) -> Self {
ExpectedSpan::default().with_id(id.clone())
}
}
impl From<&ExpectedSpan> for ExpectedSpan {
fn from(span: &ExpectedSpan) -> Self {
span.clone()
}
}
/// A mock new span.
///
/// **Note**: This struct contains expectations that can only be asserted
@ -166,7 +206,8 @@ pub struct ExpectedId {
impl ExpectedSpan {
/// Sets a name to expect when matching a span.
///
/// If an event is recorded with a name that differs from the one provided to this method, the expectation will fail.
/// If an event is recorded with a name that differs from the one provided to this method, the
/// expectation will fail.
///
/// # Examples
///
@ -187,6 +228,25 @@ impl ExpectedSpan {
/// handle.assert_finished();
/// ```
///
/// If only the name of the span needs to be validated, then
/// instead of using the `named` method, a string can be passed
/// to the [`MockCollector`] functions directly.
///
/// ```
/// use tracing_mock::subscriber;
///
/// let (subscriber, handle) = subscriber::mock()
/// .enter("span name")
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
/// let span = tracing::info_span!("span name");
/// let _guard = span.enter();
/// });
///
/// handle.assert_finished();
/// ```
///
/// When the span name is different, the assertion will fail:
///
/// ```should_panic
@ -205,6 +265,8 @@ impl ExpectedSpan {
///
/// handle.assert_finished();
/// ```
///
/// [`MockCollector`]: struct@crate::subscriber::MockCollector
pub fn named<I>(self, name: I) -> Self
where
I: Into<String>,
@ -247,10 +309,41 @@ impl ExpectedSpan {
/// let span2 = expect::span().named("span").with_id(id2.clone());
///
/// let (subscriber, handle) = subscriber::mock()
/// .new_span(span1.clone())
/// .new_span(span2.clone())
/// .enter(span2)
/// .enter(span1)
/// .new_span(&span1)
/// .new_span(&span2)
/// .enter(&span2)
/// .enter(&span1)
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
/// fn create_span() -> tracing::Span {
/// tracing::info_span!("span")
/// }
///
/// let span1 = create_span();
/// let span2 = create_span();
///
/// let _guard2 = span2.enter();
/// let _guard1 = span1.enter();
/// });
///
/// handle.assert_finished();
/// ```
///
/// Since `ExpectedId` implements `Into<ExpectedSpan>`, in cases where
/// only checking on Id is desired, a shorthand version of the previous
/// example can be used.
///
/// ```
/// use tracing_mock::{subscriber, expect};
/// let id1 = expect::id();
/// let id2 = expect::id();
///
/// let (subscriber, handle) = subscriber::mock()
/// .new_span(&id1)
/// .new_span(&id2)
/// .enter(&id2)
/// .enter(&id1)
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
@ -279,10 +372,10 @@ impl ExpectedSpan {
/// let span2 = expect::span().named("span").with_id(id2.clone());
///
/// let (subscriber, handle) = subscriber::mock()
/// .new_span(span1.clone())
/// .new_span(span2.clone())
/// .enter(span2)
/// .enter(span1)
/// .new_span(&span1)
/// .new_span(&span2)
/// .enter(&span2)
/// .enter(&span1)
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
@ -496,8 +589,8 @@ impl ExpectedSpan {
/// .with_ancestry(expect::has_explicit_parent("parent_span"));
///
/// let (subscriber, handle) = subscriber::mock()
/// .new_span(parent_span.clone())
/// .enter(parent_span)
/// .new_span(&parent_span)
/// .enter(&parent_span)
/// .new_span(span)
/// .run_with_handle();
///
@ -530,26 +623,6 @@ impl ExpectedSpan {
/// handle.assert_finished();
/// ```
///
/// In the following example, we expect that the matched span is
/// a contextually-determined root:
///
/// ```
/// use tracing_mock::{subscriber, expect};
///
/// let span = expect::span()
/// .with_ancestry(expect::is_contextual_root());
///
/// let (subscriber, handle) = subscriber::mock()
/// .new_span(span)
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
/// tracing::info_span!("span");
/// });
///
/// handle.assert_finished();
/// ```
///
/// In the example below, the expectation fails because the
/// span is *contextually*—as opposed to explicitly—within the span
/// `parent_span`:
@ -562,8 +635,8 @@ impl ExpectedSpan {
/// .with_ancestry(expect::has_explicit_parent("parent_span"));
///
/// let (subscriber, handle) = subscriber::mock()
/// .new_span(parent_span.clone())
/// .enter(parent_span)
/// .new_span(&parent_span)
/// .enter(&parent_span)
/// .new_span(span)
/// .run_with_handle();
///
@ -719,10 +792,13 @@ impl fmt::Display for ExpectedSpan {
}
}
impl From<ExpectedSpan> for NewSpan {
fn from(span: ExpectedSpan) -> Self {
impl<S> From<S> for NewSpan
where
S: Into<ExpectedSpan>,
{
fn from(span: S) -> Self {
Self {
span,
span: span.into(),
..Default::default()
}
}

View File

@ -38,13 +38,13 @@
//! .named("my_span");
//! let (subscriber, handle) = subscriber::mock()
//! // Enter a matching span
//! .enter(span.clone())
//! .enter(&span)
//! // Record an event with message "collect parting message"
//! .event(expect::event().with_fields(expect::message("collect parting message")))
//! // Record a value for the field `parting` on a matching span
//! .record(span.clone(), expect::field("parting").with_value(&"goodbye world!"))
//! // Exit a matching span
//! .exit(span)
//! .exit(&span)
//! // Expect no further messages to be recorded
//! .only()
//! // Return the subscriber and handle
@ -80,9 +80,10 @@
//! let span = expect::span()
//! .named("my_span");
//! let (subscriber, handle) = subscriber::mock()
//! .enter(span.clone())
//! // Enter a matching span
//! .enter(&span)
//! .event(expect::event().with_fields(expect::message("collect parting message")))
//! .record(span.clone(), expect::field("parting").with_value(&"goodbye world!"))
//! .record(&span, expect::field("parting").with_value(&"goodbye world!"))
//! .exit(span)
//! .only()
//! .run_with_handle();
@ -225,13 +226,13 @@ pub struct MockHandle(Arc<Mutex<VecDeque<Expect>>>, String);
/// .named("my_span");
/// let (subscriber, handle) = subscriber::mock()
/// // Enter a matching span
/// .enter(span.clone())
/// .enter(&span)
/// // Record an event with message "collect parting message"
/// .event(expect::event().with_fields(expect::message("collect parting message")))
/// // Record a value for the field `parting` on a matching span
/// .record(span.clone(), expect::field("parting").with_value(&"goodbye world!"))
/// // Exit a matching span
/// .exit(span)
/// .exit(&span)
/// // Expect no further messages to be recorded
/// .only()
/// // Return the subscriber and handle
@ -472,8 +473,8 @@ where
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (subscriber, handle) = subscriber::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .only()
/// .run_with_handle();
///
@ -495,8 +496,8 @@ where
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (subscriber, handle) = subscriber::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .only()
/// .run_with_handle();
///
@ -511,8 +512,11 @@ where
///
/// [`exit`]: fn@Self::exit
/// [`only`]: fn@Self::only
pub fn enter(mut self, span: ExpectedSpan) -> Self {
self.expected.push_back(Expect::Enter(span));
pub fn enter<S>(mut self, span: S) -> Self
where
S: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::Enter(span.into()));
self
}
@ -536,8 +540,8 @@ where
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (subscriber, handle) = subscriber::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
@ -558,8 +562,8 @@ where
/// .at_level(tracing::Level::INFO)
/// .named("the span we're testing");
/// let (subscriber, handle) = subscriber::mock()
/// .enter(span.clone())
/// .exit(span)
/// .enter(&span)
/// .exit(&span)
/// .run_with_handle();
///
/// tracing::subscriber::with_default(subscriber, || {
@ -572,8 +576,11 @@ where
/// ```
///
/// [`enter`]: fn@Self::enter
pub fn exit(mut self, span: ExpectedSpan) -> Self {
self.expected.push_back(Expect::Exit(span));
pub fn exit<S>(mut self, span: S) -> Self
where
S: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::Exit(span.into()));
self
}
@ -627,8 +634,11 @@ where
///
/// handle.assert_finished();
/// ```
pub fn clone_span(mut self, span: ExpectedSpan) -> Self {
self.expected.push_back(Expect::CloneSpan(span));
pub fn clone_span<S>(mut self, span: S) -> Self
where
S: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::CloneSpan(span.into()));
self
}
@ -644,8 +654,11 @@ where
///
/// [`Subscriber::drop_span`]: fn@tracing::Subscriber::drop_span
#[allow(deprecated)]
pub fn drop_span(mut self, span: ExpectedSpan) -> Self {
self.expected.push_back(Expect::DropSpan(span));
pub fn drop_span<S>(mut self, span: S) -> Self
where
S: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::DropSpan(span.into()));
self
}
@ -710,9 +723,15 @@ where
/// ```
///
/// [`Span::follows_from`]: fn@tracing::Span::follows_from
pub fn follows_from(mut self, consequence: ExpectedSpan, cause: ExpectedSpan) -> Self {
self.expected
.push_back(Expect::FollowsFrom { consequence, cause });
pub fn follows_from<S1, S2>(mut self, consequence: S1, cause: S2) -> Self
where
S1: Into<ExpectedSpan>,
S2: Into<ExpectedSpan>,
{
self.expected.push_back(Expect::FollowsFrom {
consequence: consequence.into(),
cause: cause.into(),
});
self
}
@ -775,11 +794,13 @@ where
/// ```
///
/// [`field`]: mod@crate::field
pub fn record<I>(mut self, span: ExpectedSpan, fields: I) -> Self
pub fn record<S, I>(mut self, span: S, fields: I) -> Self
where
S: Into<ExpectedSpan>,
I: Into<ExpectedFields>,
{
self.expected.push_back(Expect::Visit(span, fields.into()));
self.expected
.push_back(Expect::Visit(span.into(), fields.into()));
self
}