1use core::fmt;
4
5#[cfg(feature = "tracing")]
7#[derive(Debug, Clone)]
8pub struct SpanTrace {
9 inner: tracing_error::SpanTrace,
10}
11
12#[cfg(feature = "tracing")]
13impl SpanTrace {
14 #[must_use]
20 #[inline]
21 pub fn capture() -> Self {
22 Self {
23 inner: tracing_error::SpanTrace::capture(),
24 }
25 }
26
27 #[must_use]
29 #[inline]
30 pub const fn new(inner: tracing_error::SpanTrace) -> Self {
31 Self { inner }
32 }
33
34 #[must_use]
36 #[inline]
37 pub fn status(&self) -> SpanTraceStatus {
38 self.inner.status().into()
39 }
40
41 #[must_use]
45 #[inline]
46 pub fn is_captured(&self) -> bool {
47 matches!(self.status(), SpanTraceStatus::Captured)
48 }
49
50 #[must_use]
52 #[inline]
53 pub fn into_span_trace(self) -> tracing_error::SpanTrace {
54 self.inner
55 }
56
57 #[inline]
59 #[must_use]
60 pub const fn as_span_trace(&self) -> &tracing_error::SpanTrace {
61 &self.inner
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum SpanTraceStatus {
68 Unsupported,
71 Empty,
74 Captured,
76}
77
78#[cfg(feature = "tracing")]
79impl From<tracing_error::SpanTraceStatus> for SpanTraceStatus {
80 fn from(status: tracing_error::SpanTraceStatus) -> Self {
81 if status == tracing_error::SpanTraceStatus::CAPTURED {
82 Self::Captured
83 } else if status == tracing_error::SpanTraceStatus::EMPTY {
84 Self::Empty
85 } else {
86 Self::Unsupported
87 }
88 }
89}
90
91#[cfg(all(feature = "tracing", debug_assertions))]
94#[inline]
95fn capture_fields(fields: &str) -> String {
96 fields.to_owned()
97}
98
99#[cfg(all(feature = "tracing", not(debug_assertions)))]
100#[inline]
101fn capture_fields(_fields: &str) {}
102
103#[cfg(all(feature = "tracing", debug_assertions))]
104#[inline]
105fn fields_eq(stored: &str, current: &str) -> bool {
106 stored == current
107}
108
109#[cfg(all(feature = "tracing", not(debug_assertions)))]
110#[inline]
111fn fields_eq(_stored: &(), _current: &str) -> bool {
112 true
113}
114
115#[cfg(feature = "tracing")]
116impl PartialEq for SpanTrace {
117 fn eq(&self, other: &Self) -> bool {
118 use std::collections::VecDeque;
119
120 let a = &self.inner;
121 let b = &other.inner;
122
123 let mut a_frames = VecDeque::with_capacity(2);
124 a.with_spans(|a_md, a_fields| {
125 a_frames.push_back((a_md.callsite(), capture_fields(a_fields)));
126 true
127 });
128 let mut equal = true;
129 b.with_spans(|b_md, b_fields| {
130 equal = match a_frames.pop_front() {
131 Some((a_callsite, a_fields)) => {
132 a_callsite == b_md.callsite() && fields_eq(&a_fields, b_fields)
133 }
134 None => false,
135 };
136 equal
137 });
138 equal && a_frames.is_empty()
139 }
140}
141
142#[cfg(feature = "tracing")]
143impl fmt::Display for SpanTrace {
144 #[inline]
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 fmt::Display::fmt(&self.inner, f)
147 }
148}
149
150#[cfg(not(feature = "tracing"))]
154#[derive(Clone, Debug)]
155pub struct SpanTrace;
156
157#[cfg(not(feature = "tracing"))]
158impl SpanTrace {
159 #[must_use]
161 #[inline]
162 pub const fn capture() -> Self {
163 Self
164 }
165
166 #[must_use]
168 #[inline]
169 pub const fn is_captured(&self) -> bool {
170 false
171 }
172}
173
174#[cfg(not(feature = "tracing"))]
175impl PartialEq for SpanTrace {
176 #[inline]
177 fn eq(&self, _other: &Self) -> bool {
178 true
179 }
180}
181
182#[cfg(not(feature = "tracing"))]
183impl fmt::Display for SpanTrace {
184 #[inline]
185 fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 Ok(())
187 }
188}
189
190impl crate::Capturable for SpanTrace {
191 #[inline]
192 fn capture() -> Self {
193 Self::capture()
194 }
195
196 #[inline]
197 fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
198 match source.oopsie_spantrace() {
199 Some(trace) if trace.is_captured() => trace.clone(),
203 _ => Self::capture(),
204 }
205 }
206}
207
208#[derive(Clone, Debug, Default)]
217pub struct OptionalSpanTrace(Option<SpanTrace>);
218
219impl OptionalSpanTrace {
220 #[must_use]
222 #[inline]
223 pub const fn none() -> Self {
224 Self(None)
225 }
226
227 #[must_use]
229 #[inline]
230 pub const fn some(trace: SpanTrace) -> Self {
231 Self(Some(trace))
232 }
233
234 #[cfg(feature = "tracing")]
238 #[must_use]
239 #[inline]
240 pub fn into_inner(self) -> Option<SpanTrace> {
241 self.0
242 }
243
244 #[cfg(not(feature = "tracing"))]
246 #[must_use]
247 #[inline]
248 pub const fn into_inner(self) -> Option<SpanTrace> {
249 self.0
250 }
251
252 #[must_use]
254 #[inline]
255 pub const fn as_ref(&self) -> Option<&SpanTrace> {
256 self.0.as_ref()
257 }
258
259 #[must_use]
261 #[inline]
262 pub const fn is_some(&self) -> bool {
263 self.0.is_some()
264 }
265
266 #[must_use]
268 #[inline]
269 pub const fn is_none(&self) -> bool {
270 self.0.is_none()
271 }
272}
273
274impl fmt::Display for OptionalSpanTrace {
275 #[inline]
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 match &self.0 {
278 Some(trace) => fmt::Display::fmt(trace, f),
279 None => Ok(()),
280 }
281 }
282}
283
284impl crate::Capturable for OptionalSpanTrace {
285 #[inline]
286 fn capture() -> Self {
287 let trace = SpanTrace::capture();
288 if trace.is_captured() {
289 Self(Some(trace))
290 } else {
291 Self(None)
292 }
293 }
294
295 #[inline]
296 fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
297 match source.oopsie_spantrace() {
298 Some(trace) if trace.is_captured() => Self::some(trace.clone()),
302 _ => <Self as crate::Capturable>::capture(),
303 }
304 }
305}
306
307impl From<SpanTrace> for OptionalSpanTrace {
308 #[inline]
309 fn from(trace: SpanTrace) -> Self {
310 Self(Some(trace))
311 }
312}
313
314impl From<Option<SpanTrace>> for OptionalSpanTrace {
315 #[inline]
316 fn from(opt: Option<SpanTrace>) -> Self {
317 Self(opt)
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::Capturable;
325
326 #[test]
327 fn test_span_trace_capture() {
328 let trace = SpanTrace::capture();
329 let _ = trace.is_captured();
330 }
331
332 #[test]
333 fn test_generate_implicit_data() {
334 let trace: SpanTrace = Capturable::capture();
335 let _ = trace.is_captured();
336 }
337
338 #[test]
339 fn test_optional_span_trace() {
340 let trace: OptionalSpanTrace = Capturable::capture();
341 let _ = trace;
342 }
343
344 #[cfg(feature = "tracing")]
345 #[test]
346 fn test_into_span_trace_tracing_returns_some() {
347 let tracing = SpanTrace::capture();
348 let _ = tracing.into_span_trace();
349 }
350
351 #[test]
352 fn test_optional_span_trace_none_into_inner() {
353 let opt = OptionalSpanTrace::none();
354 assert!(opt.into_inner().is_none());
355 }
356
357 #[test]
358 fn test_optional_span_trace_as_ref_none() {
359 let opt = OptionalSpanTrace::none();
360 assert!(opt.as_ref().is_none());
361 }
362
363 #[test]
364 fn test_optional_span_trace_is_none() {
365 let opt = OptionalSpanTrace::none();
366 assert!(opt.is_none());
367 assert!(!opt.is_some());
368 }
369
370 #[test]
371 fn test_optional_span_trace_display_none() {
372 let opt = OptionalSpanTrace::none();
373 let display = opt.to_string();
374 assert!(display.is_empty());
375 }
376
377 #[test]
378 fn test_optional_span_trace_from_option_none() {
379 let opt: OptionalSpanTrace = None::<SpanTrace>.into();
380 assert!(opt.is_none());
381 }
382
383 #[test]
384 fn test_optional_span_trace_generate_without_subscriber() {
385 let opt: OptionalSpanTrace = Capturable::capture();
386 assert!(opt.is_none());
387 }
388
389 #[test]
391 fn test_optional_span_trace_some_into_inner() {
392 let trace = SpanTrace::capture();
393 let opt = OptionalSpanTrace::some(trace);
394 assert!(opt.into_inner().is_some());
395 }
396
397 #[test]
398 fn test_optional_span_trace_as_ref_some() {
399 let trace = SpanTrace::capture();
400 let opt = OptionalSpanTrace::some(trace);
401 assert!(opt.as_ref().is_some());
402 }
403
404 #[test]
405 fn test_optional_span_trace_is_some() {
406 let trace = SpanTrace::capture();
407 let opt = OptionalSpanTrace::some(trace);
408 assert!(opt.is_some());
409 assert!(!opt.is_none());
410 }
411
412 #[test]
413 fn test_optional_span_trace_display_some() {
414 let trace = SpanTrace::capture();
415 let opt = OptionalSpanTrace::some(trace);
416 let _ = opt.to_string();
417 }
418
419 #[test]
420 fn test_optional_span_trace_from_spantrace() {
421 let trace = SpanTrace::capture();
422 let opt: OptionalSpanTrace = trace.into();
423 assert!(opt.is_some());
424 }
425
426 #[test]
427 fn test_optional_span_trace_from_option_some() {
428 let trace = SpanTrace::capture();
429 let opt: OptionalSpanTrace = Some(trace).into();
430 assert!(opt.is_some());
431 }
432
433 #[cfg(feature = "tracing")]
434 fn with_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
435 use tracing_subscriber::prelude::*;
436 let subscriber =
437 tracing_subscriber::Registry::default().with(tracing_error::ErrorLayer::default());
438 tracing::subscriber::with_default(subscriber, f)
439 }
440
441 #[cfg(feature = "tracing")]
442 fn without_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
443 let subscriber = tracing_subscriber::Registry::default();
444 tracing::subscriber::with_default(subscriber, f)
445 }
446
447 #[cfg(feature = "tracing")]
451 fn leaf() -> SpanTrace {
452 let _g = tracing::info_span!("leaf").entered();
453 SpanTrace::capture()
454 }
455 #[cfg(feature = "tracing")]
456 fn mid() -> SpanTrace {
457 let _g = tracing::info_span!("mid").entered();
458 leaf()
459 }
460 #[cfg(feature = "tracing")]
461 fn via_root_a() -> SpanTrace {
462 let _g = tracing::info_span!("root_a").entered();
463 mid()
464 }
465 #[cfg(feature = "tracing")]
466 fn via_root_b() -> SpanTrace {
467 let _g = tracing::info_span!("root_b").entered();
468 mid()
469 }
470
471 #[cfg(feature = "tracing")]
475 fn via_field(x: u32) -> SpanTrace {
476 let _g = tracing::info_span!("field_span", x).entered();
477 SpanTrace::capture()
478 }
479
480 #[cfg(feature = "tracing")]
481 #[test]
482 fn identical_depth3_stacks_are_equal() {
483 let (a, b) = with_error_subscriber(|| (via_root_a(), via_root_a()));
484 assert_eq!(a.status(), SpanTraceStatus::Captured);
485 assert_eq!(a, b);
486 assert_eq!(a, a.clone());
487 }
488
489 #[cfg(feature = "tracing")]
490 #[test]
491 fn depth3_stacks_differing_only_at_root_are_unequal() {
492 let (a, b) = with_error_subscriber(|| (via_root_a(), via_root_b()));
495 assert_ne!(a, b);
496 }
497
498 #[cfg(feature = "tracing")]
499 #[test]
500 fn shorter_stack_is_not_equal_to_deeper_one() {
501 let (deep, shallow) = with_error_subscriber(|| (via_root_a(), leaf()));
502 assert_ne!(deep, shallow);
503 assert_ne!(shallow, deep);
505 }
506
507 #[cfg(feature = "tracing")]
508 #[test]
509 fn empty_span_traces_are_equal() {
510 let a = SpanTrace::capture();
513 let b = SpanTrace::capture();
514 assert_eq!(a.status(), SpanTraceStatus::Empty);
515 assert_eq!(b.status(), SpanTraceStatus::Empty);
516 assert_eq!(a, b);
517 assert_eq!(a, a.clone());
518 }
519
520 #[cfg(feature = "tracing")]
521 #[test]
522 fn unsupported_span_traces() {
523 let (a, b) = without_error_subscriber(|| (via_root_a(), via_root_b()));
526
527 assert_eq!(a.status(), SpanTraceStatus::Unsupported);
528 assert_eq!(b.status(), SpanTraceStatus::Unsupported);
529 assert_eq!(a, b);
530 }
531
532 #[cfg(feature = "tracing")]
533 #[test]
534 fn same_callsite_differing_field_values() {
535 let (a, b) = with_error_subscriber(|| (via_field(1), via_field(2)));
541 assert_eq!(a.status(), SpanTraceStatus::Captured);
542
543 let (c, d) = with_error_subscriber(|| (via_field(1), via_field(1)));
547 assert_eq!(c, d);
548
549 #[cfg(debug_assertions)]
552 assert_ne!(a, b, "debug builds must distinguish differing field values");
553 #[cfg(not(debug_assertions))]
554 assert_eq!(
555 a, b,
556 "release builds compare callsites only, ignoring fields"
557 );
558 }
559
560 #[derive(Debug)]
561 struct DiagSource;
562
563 impl fmt::Display for DiagSource {
564 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565 f.write_str("diag source")
566 }
567 }
568
569 impl std::error::Error for DiagSource {}
570 impl crate::Diagnostic for DiagSource {}
571
572 #[test]
573 fn optional_span_trace_captures_from_diagnostic_source() {
574 let opt = <OptionalSpanTrace as crate::Capturable>::capture_or_extract(&DiagSource);
575 assert!(opt.is_none());
576 }
577
578 #[derive(Debug)]
579 struct DiagSourceWithEmptyTrace(SpanTrace);
580
581 impl fmt::Display for DiagSourceWithEmptyTrace {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 f.write_str("diag source")
584 }
585 }
586
587 impl std::error::Error for DiagSourceWithEmptyTrace {}
588 impl crate::Diagnostic for DiagSourceWithEmptyTrace {
589 fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
590 Some(&self.0)
591 }
592 }
593
594 #[test]
595 fn capture_or_extract_drops_empty_source_trace() {
596 let src = DiagSourceWithEmptyTrace(SpanTrace::capture());
599 assert!(!src.0.is_captured(), "precondition: source trace is empty");
600 let opt = <OptionalSpanTrace as crate::Capturable>::capture_or_extract(&src);
601 assert!(opt.is_none());
602 }
603
604 #[cfg(feature = "tracing")]
605 #[test]
606 fn span_trace_capture_or_extract_recaptures_over_empty_source_trace() {
607 let src = DiagSourceWithEmptyTrace(SpanTrace::capture());
609 assert!(!src.0.is_captured(), "precondition: source trace is empty");
610 let extracted = with_error_subscriber(|| {
613 let _g = tracing::info_span!("wrap_site").entered();
614 <SpanTrace as crate::Capturable>::capture_or_extract(&src)
615 });
616 assert!(extracted.is_captured());
617 }
618
619 #[cfg(feature = "tracing")]
620 #[test]
621 fn span_trace_capture_or_extract_keeps_captured_source_trace() {
622 let (src_trace, extracted) = with_error_subscriber(|| {
623 let src = DiagSourceWithEmptyTrace(leaf());
624 let t = <SpanTrace as crate::Capturable>::capture_or_extract(&src);
625 (src.0, t)
626 });
627 assert!(extracted.is_captured());
628 assert_eq!(extracted, src_trace);
629 }
630
631 #[cfg(not(feature = "tracing"))]
632 mod stub {
633 use super::*;
634
635 #[test]
636 fn capture_is_inert() {
637 assert!(!SpanTrace::capture().is_captured());
638 }
639
640 #[test]
641 fn display_is_empty() {
642 assert!(SpanTrace::capture().to_string().is_empty());
643 }
644
645 #[test]
646 fn equality_is_reflexive() {
647 let a = SpanTrace::capture();
648 assert_eq!(a, a.clone());
649 assert_eq!(SpanTrace::capture(), SpanTrace::capture());
650 }
651
652 #[test]
653 fn capture_or_extract_yields_stub() {
654 let extracted = <SpanTrace as crate::Capturable>::capture_or_extract(
655 &DiagSourceWithEmptyTrace(SpanTrace::capture()),
656 );
657 assert!(!extracted.is_captured());
658 }
659
660 #[test]
661 fn optional_capture_is_none() {
662 let opt: OptionalSpanTrace = Capturable::capture();
663 assert!(opt.is_none());
664 }
665 }
666}