Skip to content

Commit ba387dd

Browse files
authored
mock: add ExpectedId to link span expectations (#3007)
## Motivation It currently isn't possible to differentiate spans with the same name, target, and level when setting expectations on `enter`, `exit`, and `drop_span`. This is not an issue for `tracing-mock`'s original (and still primary) use case, which is to test `tracing` itself. However, when testing the tracing instrumentation in library or application code, this can be a limitation. For example, when testing the instrumentation in tokio (tokio-rs/tokio#6112), it isn't possible to set an expectation on which task span is entered first, because the name, target, and level of those spans are always identical - in fact, the spans have the same metadata and only the field values are different. ## Solution To make differentiating different spans possible, `ExpectId` has been introduced. It is an opaque struct which represents a `span::Id` and can be used to match spans from a `new_span` expectation (where a `NewSpan` is accepted and all fields and values can be expected) through to subsequent `enter`, `exit`, and `drop_span` expectations. An `ExpectedId` is passed to an `ExpectedSpan` which then needs to be expected with `MockCollector::new_span`. A clone of the `ExpectedId` (or a clone of the `ExpectedSpan` with the `ExpectedId` already on it) will then match the ID assigned to the span to the other span lifecycle expectations. The `ExpectedId` uses an `Arc<AtomicU64>` which has the ID for the new span assigned to it, and then its clones will be matched against that same ID. In future changes it will also be possible to use this `ExpectedId` to match parent spans, currently a parent is only matched by name.
1 parent 382ee01 commit ba387dd

File tree

3 files changed

+222
-2
lines changed

3 files changed

+222
-2
lines changed

tracing-mock/src/collector.rs

+9
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,17 @@ use tracing::{
159159
};
160160

161161
pub(crate) struct SpanState {
162+
id: u64,
162163
name: &'static str,
163164
refs: usize,
164165
meta: &'static Metadata<'static>,
165166
}
166167

167168
impl SpanState {
169+
pub(crate) fn id(&self) -> u64 {
170+
self.id
171+
}
172+
168173
pub(crate) fn metadata(&self) -> &'static Metadata<'static> {
169174
self.meta
170175
}
@@ -1100,6 +1105,9 @@ where
11001105
let mut spans = self.spans.lock().unwrap();
11011106
if was_expected {
11021107
if let Expect::NewSpan(mut expected) = expected.pop_front().unwrap() {
1108+
if let Some(expected_id) = &expected.span.id {
1109+
expected_id.set(id.into_u64()).unwrap();
1110+
}
11031111
let get_parent_name = || {
11041112
let stack = self.current.lock().unwrap();
11051113
span.parent()
@@ -1113,6 +1121,7 @@ where
11131121
spans.insert(
11141122
id.clone(),
11151123
SpanState {
1124+
id: id.into_u64(),
11161125
name: meta.name(),
11171126
refs: 1,
11181127
meta,

tracing-mock/src/expect.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::fmt;
33
use crate::{
44
event::ExpectedEvent,
55
field::{ExpectedField, ExpectedFields, ExpectedValue},
6-
span::{ExpectedSpan, NewSpan},
6+
span::{ExpectedId, ExpectedSpan, NewSpan},
77
};
88

99
#[derive(Debug, Eq, PartialEq)]
@@ -51,6 +51,25 @@ pub fn span() -> ExpectedSpan {
5151
}
5252
}
5353

54+
/// Returns a new, unset `ExpectedId`.
55+
///
56+
/// The `ExpectedId` needs to be attached to a [`NewSpan`] or an
57+
/// [`ExpectedSpan`] passed to [`MockCollector::new_span`] to
58+
/// ensure that it gets set. When the a clone of the same
59+
/// `ExpectedSpan` is attached to an [`ExpectedSpan`] and passed to
60+
/// any other method on [`MockCollector`] that accepts it, it will
61+
/// ensure that it is exactly the same span used across those
62+
/// distinct expectations.
63+
///
64+
/// For more details on how to use this struct, see the documentation
65+
/// on [`ExpectedSpan::with_id`].
66+
///
67+
/// [`MockCollector`]: struct@crate::collector::MockCollector
68+
/// [`MockCollector::new_span`]: fn@crate::collector::MockCollector::new_span
69+
pub fn id() -> ExpectedId {
70+
ExpectedId::new_unset()
71+
}
72+
5473
impl Expect {
5574
pub(crate) fn bad(&self, name: impl AsRef<str>, what: fmt::Arguments<'_>) {
5675
let name = name.as_ref();

tracing-mock/src/span.rs

+193-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,13 @@
9494
use crate::{
9595
collector::SpanState, expect, field::ExpectedFields, metadata::ExpectedMetadata, Parent,
9696
};
97-
use std::fmt;
97+
use std::{
98+
error, fmt,
99+
sync::{
100+
atomic::{AtomicU64, Ordering},
101+
Arc,
102+
},
103+
};
98104

99105
/// A mock span.
100106
///
@@ -104,6 +110,7 @@ use std::fmt;
104110
/// [`collector`]: mod@crate::collector
105111
#[derive(Clone, Default, Eq, PartialEq)]
106112
pub struct ExpectedSpan {
113+
pub(crate) id: Option<ExpectedId>,
107114
pub(crate) metadata: ExpectedMetadata,
108115
}
109116

@@ -137,6 +144,24 @@ where
137144
expect::span().named(name)
138145
}
139146

147+
/// A mock span ID.
148+
///
149+
/// This ID makes it possible to link together calls to different
150+
/// [`MockCollector`] span methods that take an [`ExpectedSpan`] in
151+
/// addition to those that take a [`NewSpan`].
152+
///
153+
/// Use [`expect::id`] to construct a new, unset `ExpectedId`.
154+
///
155+
/// For more details on how to use this struct, see the documentation
156+
/// on [`ExpectedSpan::with_id`].
157+
///
158+
/// [`expect::id`]: fn@crate::expect::id
159+
/// [`MockCollector`]: struct@crate::collector::MockCollector
160+
#[derive(Clone, Default)]
161+
pub struct ExpectedId {
162+
inner: Arc<AtomicU64>,
163+
}
164+
140165
impl ExpectedSpan {
141166
/// Sets a name to expect when matching a span.
142167
///
@@ -188,6 +213,100 @@ impl ExpectedSpan {
188213
name: Some(name.into()),
189214
..self.metadata
190215
},
216+
..self
217+
}
218+
}
219+
220+
/// Sets the `ID` to expect when matching a span.
221+
///
222+
/// The [`ExpectedId`] can be used to differentiate spans that are
223+
/// otherwise identical. An [`ExpectedId`] needs to be attached to
224+
/// an `ExpectedSpan` or [`NewSpan`] which is passed to
225+
/// [`MockCollector::new_span`]. The same [`ExpectedId`] can then
226+
/// be used to match the exact same span when passed to
227+
/// [`MockCollector::enter`], [`MockCollector::exit`], and
228+
/// [`MockCollector::drop_span`].
229+
///
230+
/// This is especially useful when `tracing-mock` is being used to
231+
/// test the traces being generated within your own crate, in which
232+
/// case you may need to distinguish between spans which have
233+
/// identical metadata but different field values, which can
234+
/// otherwise only be checked in [`MockCollector::new_span`].
235+
///
236+
/// # Examples
237+
///
238+
/// Here we expect that the span that is created first is entered
239+
/// second:
240+
///
241+
/// ```
242+
/// use tracing_mock::{collector, expect};
243+
/// let id1 = expect::id();
244+
/// let span1 = expect::span().named("span").with_id(id1.clone());
245+
/// let id2 = expect::id();
246+
/// let span2 = expect::span().named("span").with_id(id2.clone());
247+
///
248+
/// let (collector, handle) = collector::mock()
249+
/// .new_span(span1.clone())
250+
/// .new_span(span2.clone())
251+
/// .enter(span2)
252+
/// .enter(span1)
253+
/// .run_with_handle();
254+
///
255+
/// tracing::collect::with_default(collector, || {
256+
/// fn create_span() -> tracing::Span {
257+
/// tracing::info_span!("span")
258+
/// }
259+
///
260+
/// let span1 = create_span();
261+
/// let span2 = create_span();
262+
///
263+
/// let _guard2 = span2.enter();
264+
/// let _guard1 = span1.enter();
265+
/// });
266+
///
267+
/// handle.assert_finished();
268+
/// ```
269+
///
270+
/// If the order that the spans are entered changes, the test will
271+
/// fail:
272+
///
273+
/// ```should_panic
274+
/// use tracing_mock::{collector, expect};
275+
/// let id1 = expect::id();
276+
/// let span1 = expect::span().named("span").with_id(id1.clone());
277+
/// let id2 = expect::id();
278+
/// let span2 = expect::span().named("span").with_id(id2.clone());
279+
///
280+
/// let (collector, handle) = collector::mock()
281+
/// .new_span(span1.clone())
282+
/// .new_span(span2.clone())
283+
/// .enter(span2)
284+
/// .enter(span1)
285+
/// .run_with_handle();
286+
///
287+
/// tracing::collect::with_default(collector, || {
288+
/// fn create_span() -> tracing::Span {
289+
/// tracing::info_span!("span")
290+
/// }
291+
///
292+
/// let span1 = create_span();
293+
/// let span2 = create_span();
294+
///
295+
/// let _guard1 = span1.enter();
296+
/// let _guard2 = span2.enter();
297+
/// });
298+
///
299+
/// handle.assert_finished();
300+
/// ```
301+
///
302+
/// [`MockCollector::new_span`]: fn@crate::collector::MockCollector::new_span
303+
/// [`MockCollector::enter`]: fn@crate::collector::MockCollector::enter
304+
/// [`MockCollector::exit`]: fn@crate::collector::MockCollector::exit
305+
/// [`MockCollector::drop_span`]: fn@crate::collector::MockCollector::drop_span
306+
pub fn with_id(self, id: ExpectedId) -> Self {
307+
Self {
308+
id: Some(id),
309+
..self
191310
}
192311
}
193312

@@ -241,6 +360,7 @@ impl ExpectedSpan {
241360
level: Some(level),
242361
..self.metadata
243362
},
363+
..self
244364
}
245365
}
246366

@@ -297,6 +417,7 @@ impl ExpectedSpan {
297417
target: Some(target.into()),
298418
..self.metadata
299419
},
420+
..self
300421
}
301422
}
302423

@@ -598,6 +719,11 @@ impl ExpectedSpan {
598719
pub(crate) fn check(&self, actual: &SpanState, collector_name: &str) {
599720
let meta = actual.metadata();
600721
let name = meta.name();
722+
723+
if let Some(expected_id) = &self.id {
724+
expected_id.check(actual.id(), format_args!("span `{}`", name), collector_name);
725+
}
726+
601727
self.metadata
602728
.check(meta, format_args!("span `{}`", name), collector_name);
603729
}
@@ -760,3 +886,69 @@ impl fmt::Debug for NewSpan {
760886
s.finish()
761887
}
762888
}
889+
890+
impl PartialEq for ExpectedId {
891+
fn eq(&self, other: &Self) -> bool {
892+
self.inner.load(Ordering::Relaxed) == other.inner.load(Ordering::Relaxed)
893+
}
894+
}
895+
896+
impl Eq for ExpectedId {}
897+
898+
impl ExpectedId {
899+
const UNSET: u64 = 0;
900+
901+
pub(crate) fn new_unset() -> Self {
902+
Self {
903+
inner: Arc::new(AtomicU64::from(Self::UNSET)),
904+
}
905+
}
906+
907+
pub(crate) fn set(&self, span_id: u64) -> Result<(), SetActualSpanIdError> {
908+
self.inner
909+
.compare_exchange(Self::UNSET, span_id, Ordering::Relaxed, Ordering::Relaxed)
910+
.map_err(|current| SetActualSpanIdError {
911+
previous_span_id: current,
912+
new_span_id: span_id,
913+
})?;
914+
Ok(())
915+
}
916+
917+
pub(crate) fn check(&self, actual: u64, ctx: fmt::Arguments<'_>, collector_name: &str) {
918+
let id = self.inner.load(Ordering::Relaxed);
919+
920+
assert!(
921+
id != Self::UNSET,
922+
"\n[{}] expected {} to have expected ID set, but it hasn't been, \
923+
perhaps this `ExpectedId` wasn't used in a call to `MockCollector::new_span()`?",
924+
collector_name,
925+
ctx,
926+
);
927+
928+
assert_eq!(
929+
id, actual,
930+
"\n[{}] expected {} to have ID `{}`, but it has `{}` instead",
931+
collector_name, ctx, id, actual,
932+
);
933+
}
934+
}
935+
936+
#[derive(Debug)]
937+
pub(crate) struct SetActualSpanIdError {
938+
previous_span_id: u64,
939+
new_span_id: u64,
940+
}
941+
942+
impl fmt::Display for SetActualSpanIdError {
943+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944+
write!(
945+
f,
946+
"Could not set `ExpecedId` to {new}, \
947+
it had already been set to {previous}",
948+
new = self.new_span_id,
949+
previous = self.previous_span_id
950+
)
951+
}
952+
}
953+
954+
impl error::Error for SetActualSpanIdError {}

0 commit comments

Comments
 (0)