1use std::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) -> tracing_error::SpanTraceStatus {
38 self.inner.status()
39 }
40
41 #[must_use]
45 #[inline]
46 pub fn is_captured(&self) -> bool {
47 matches!(self.status(), tracing_error::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#[cfg(all(feature = "tracing", debug_assertions))]
68#[inline]
69fn capture_fields(fields: &str) -> String {
70 fields.to_owned()
71}
72
73#[cfg(all(feature = "tracing", not(debug_assertions)))]
74#[inline]
75fn capture_fields(_fields: &str) {}
76
77#[cfg(all(feature = "tracing", debug_assertions))]
78#[inline]
79fn fields_eq(stored: &str, current: &str) -> bool {
80 stored == current
81}
82
83#[cfg(all(feature = "tracing", not(debug_assertions)))]
84#[inline]
85fn fields_eq(_stored: &(), _current: &str) -> bool {
86 true
87}
88
89#[cfg(feature = "tracing")]
90impl PartialEq for SpanTrace {
91 fn eq(&self, other: &Self) -> bool {
92 use std::collections::VecDeque;
93
94 let a = &self.inner;
95 let b = &other.inner;
96
97 let mut a_frames = VecDeque::with_capacity(2);
98 a.with_spans(|a_md, a_fields| {
99 a_frames.push_back((a_md.callsite(), capture_fields(a_fields)));
100 true
101 });
102 let mut equal = true;
103 b.with_spans(|b_md, b_fields| {
104 equal = match a_frames.pop_front() {
105 Some((a_callsite, a_fields)) => {
106 a_callsite == b_md.callsite() && fields_eq(&a_fields, b_fields)
107 }
108 None => false,
109 };
110 equal
111 });
112 equal && a_frames.is_empty()
113 }
114}
115
116#[cfg(feature = "tracing")]
117impl fmt::Display for SpanTrace {
118 #[inline]
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 fmt::Display::fmt(&self.inner, f)
121 }
122}
123
124#[cfg(not(feature = "tracing"))]
128#[derive(Clone, Debug)]
129pub struct SpanTrace;
130
131#[cfg(not(feature = "tracing"))]
132impl SpanTrace {
133 #[must_use]
135 #[inline]
136 pub const fn capture() -> Self {
137 Self
138 }
139
140 #[must_use]
142 #[inline]
143 pub const fn is_captured(&self) -> bool {
144 false
145 }
146}
147
148#[cfg(not(feature = "tracing"))]
149impl PartialEq for SpanTrace {
150 #[inline]
151 fn eq(&self, _other: &Self) -> bool {
152 true
153 }
154}
155
156#[cfg(not(feature = "tracing"))]
157impl fmt::Display for SpanTrace {
158 #[inline]
159 fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 Ok(())
161 }
162}
163
164impl crate::Capturable for SpanTrace {
165 #[inline]
166 fn capture() -> Self {
167 Self::capture()
168 }
169
170 #[inline]
171 fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
172 match source.oopsie_spantrace() {
173 Some(trace) if trace.is_captured() => trace.clone(),
177 _ => Self::capture(),
178 }
179 }
180}
181
182#[derive(Clone, Debug, Default)]
191pub struct OptionalSpanTrace(Option<SpanTrace>);
192
193impl OptionalSpanTrace {
194 #[must_use]
196 #[inline]
197 pub const fn none() -> Self {
198 Self(None)
199 }
200
201 #[must_use]
203 #[inline]
204 pub const fn some(trace: SpanTrace) -> Self {
205 Self(Some(trace))
206 }
207
208 #[cfg(feature = "tracing")]
212 #[must_use]
213 #[inline]
214 pub fn into_inner(self) -> Option<SpanTrace> {
215 self.0
216 }
217
218 #[cfg(not(feature = "tracing"))]
220 #[must_use]
221 #[inline]
222 pub const fn into_inner(self) -> Option<SpanTrace> {
223 self.0
224 }
225
226 #[must_use]
228 #[inline]
229 pub const fn as_ref(&self) -> Option<&SpanTrace> {
230 self.0.as_ref()
231 }
232
233 #[must_use]
235 #[inline]
236 pub const fn is_some(&self) -> bool {
237 self.0.is_some()
238 }
239
240 #[must_use]
242 #[inline]
243 pub const fn is_none(&self) -> bool {
244 self.0.is_none()
245 }
246}
247
248impl fmt::Display for OptionalSpanTrace {
249 #[inline]
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match &self.0 {
252 Some(trace) => fmt::Display::fmt(trace, f),
253 None => Ok(()),
254 }
255 }
256}
257
258impl crate::Capturable for OptionalSpanTrace {
259 #[inline]
260 fn capture() -> Self {
261 let trace = SpanTrace::capture();
262 if trace.is_captured() {
263 Self(Some(trace))
264 } else {
265 Self(None)
266 }
267 }
268
269 #[inline]
270 fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
271 match source.oopsie_spantrace() {
272 Some(trace) if trace.is_captured() => Self::some(trace.clone()),
276 _ => <Self as crate::Capturable>::capture(),
277 }
278 }
279}
280
281impl From<SpanTrace> for OptionalSpanTrace {
282 #[inline]
283 fn from(trace: SpanTrace) -> Self {
284 Self(Some(trace))
285 }
286}
287
288impl From<Option<SpanTrace>> for OptionalSpanTrace {
289 #[inline]
290 fn from(opt: Option<SpanTrace>) -> Self {
291 Self(opt)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::Capturable;
299
300 #[test]
301 fn test_span_trace_capture() {
302 let trace = SpanTrace::capture();
303 let _ = trace.is_captured();
304 }
305
306 #[test]
307 fn test_generate_implicit_data() {
308 let trace: SpanTrace = Capturable::capture();
309 let _ = trace.is_captured();
310 }
311
312 #[test]
313 fn test_optional_span_trace() {
314 let trace: OptionalSpanTrace = Capturable::capture();
315 let _ = trace;
316 }
317
318 #[cfg(feature = "tracing")]
319 #[test]
320 fn test_into_span_trace_tracing_returns_some() {
321 let tracing = SpanTrace::capture();
322 let _ = tracing.into_span_trace();
323 }
324
325 #[test]
326 fn test_optional_span_trace_none_into_inner() {
327 let opt = OptionalSpanTrace::none();
328 assert!(opt.into_inner().is_none());
329 }
330
331 #[test]
332 fn test_optional_span_trace_as_ref_none() {
333 let opt = OptionalSpanTrace::none();
334 assert!(opt.as_ref().is_none());
335 }
336
337 #[test]
338 fn test_optional_span_trace_is_none() {
339 let opt = OptionalSpanTrace::none();
340 assert!(opt.is_none());
341 assert!(!opt.is_some());
342 }
343
344 #[test]
345 fn test_optional_span_trace_display_none() {
346 let opt = OptionalSpanTrace::none();
347 let display = opt.to_string();
348 assert!(display.is_empty());
349 }
350
351 #[test]
352 fn test_optional_span_trace_from_option_none() {
353 let opt: OptionalSpanTrace = None::<SpanTrace>.into();
354 assert!(opt.is_none());
355 }
356
357 #[test]
358 fn test_optional_span_trace_generate_without_subscriber() {
359 let opt: OptionalSpanTrace = Capturable::capture();
360 assert!(opt.is_none());
361 }
362
363 #[test]
365 fn test_optional_span_trace_some_into_inner() {
366 let trace = SpanTrace::capture();
367 let opt = OptionalSpanTrace::some(trace);
368 assert!(opt.into_inner().is_some());
369 }
370
371 #[test]
372 fn test_optional_span_trace_as_ref_some() {
373 let trace = SpanTrace::capture();
374 let opt = OptionalSpanTrace::some(trace);
375 assert!(opt.as_ref().is_some());
376 }
377
378 #[test]
379 fn test_optional_span_trace_is_some() {
380 let trace = SpanTrace::capture();
381 let opt = OptionalSpanTrace::some(trace);
382 assert!(opt.is_some());
383 assert!(!opt.is_none());
384 }
385
386 #[test]
387 fn test_optional_span_trace_display_some() {
388 let trace = SpanTrace::capture();
389 let opt = OptionalSpanTrace::some(trace);
390 let _ = opt.to_string();
391 }
392
393 #[test]
394 fn test_optional_span_trace_from_spantrace() {
395 let trace = SpanTrace::capture();
396 let opt: OptionalSpanTrace = trace.into();
397 assert!(opt.is_some());
398 }
399
400 #[test]
401 fn test_optional_span_trace_from_option_some() {
402 let trace = SpanTrace::capture();
403 let opt: OptionalSpanTrace = Some(trace).into();
404 assert!(opt.is_some());
405 }
406
407 #[cfg(feature = "tracing")]
408 fn with_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
409 use tracing_subscriber::prelude::*;
410 let subscriber =
411 tracing_subscriber::Registry::default().with(tracing_error::ErrorLayer::default());
412 tracing::subscriber::with_default(subscriber, f)
413 }
414
415 #[cfg(feature = "tracing")]
419 fn leaf() -> SpanTrace {
420 let _g = tracing::info_span!("leaf").entered();
421 SpanTrace::capture()
422 }
423 #[cfg(feature = "tracing")]
424 fn mid() -> SpanTrace {
425 let _g = tracing::info_span!("mid").entered();
426 leaf()
427 }
428 #[cfg(feature = "tracing")]
429 fn via_root_a() -> SpanTrace {
430 let _g = tracing::info_span!("root_a").entered();
431 mid()
432 }
433 #[cfg(feature = "tracing")]
434 fn via_root_b() -> SpanTrace {
435 let _g = tracing::info_span!("root_b").entered();
436 mid()
437 }
438
439 #[cfg(feature = "tracing")]
443 fn via_field(x: u32) -> SpanTrace {
444 let _g = tracing::info_span!("field_span", x).entered();
445 SpanTrace::capture()
446 }
447
448 #[cfg(feature = "tracing")]
449 #[test]
450 fn identical_depth3_stacks_are_equal() {
451 let (a, b) = with_error_subscriber(|| (via_root_a(), via_root_a()));
452 assert_eq!(a.status(), tracing_error::SpanTraceStatus::CAPTURED);
453 assert_eq!(a, b);
454 assert_eq!(a, a.clone());
455 }
456
457 #[cfg(feature = "tracing")]
458 #[test]
459 fn depth3_stacks_differing_only_at_root_are_unequal() {
460 let (a, b) = with_error_subscriber(|| (via_root_a(), via_root_b()));
463 assert_ne!(a, b);
464 }
465
466 #[cfg(feature = "tracing")]
467 #[test]
468 fn shorter_stack_is_not_equal_to_deeper_one() {
469 let (deep, shallow) = with_error_subscriber(|| (via_root_a(), leaf()));
470 assert_ne!(deep, shallow);
471 assert_ne!(shallow, deep);
473 }
474
475 #[cfg(feature = "tracing")]
476 #[test]
477 fn empty_span_traces_are_equal() {
478 let a = SpanTrace::capture();
481 let b = SpanTrace::capture();
482 assert_eq!(a, b);
483 assert_eq!(a, a.clone());
484 }
485
486 #[cfg(feature = "tracing")]
487 #[test]
488 fn same_callsite_differing_field_values() {
489 let (a, b) = with_error_subscriber(|| (via_field(1), via_field(2)));
495 assert_eq!(a.status(), tracing_error::SpanTraceStatus::CAPTURED);
496
497 let (c, d) = with_error_subscriber(|| (via_field(1), via_field(1)));
501 assert_eq!(c, d);
502
503 #[cfg(debug_assertions)]
506 assert_ne!(a, b, "debug builds must distinguish differing field values");
507 #[cfg(not(debug_assertions))]
508 assert_eq!(
509 a, b,
510 "release builds compare callsites only, ignoring fields"
511 );
512 }
513
514 #[derive(Debug)]
515 struct DiagSource;
516
517 impl fmt::Display for DiagSource {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 f.write_str("diag source")
520 }
521 }
522
523 impl std::error::Error for DiagSource {}
524 impl crate::Diagnostic for DiagSource {}
525
526 #[test]
527 fn optional_span_trace_captures_from_diagnostic_source() {
528 let opt = <OptionalSpanTrace as crate::Capturable>::capture_or_extract(&DiagSource);
529 assert!(opt.is_none());
530 }
531
532 #[derive(Debug)]
533 struct DiagSourceWithEmptyTrace(SpanTrace);
534
535 impl fmt::Display for DiagSourceWithEmptyTrace {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 f.write_str("diag source")
538 }
539 }
540
541 impl std::error::Error for DiagSourceWithEmptyTrace {}
542 impl crate::Diagnostic for DiagSourceWithEmptyTrace {
543 fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
544 Some(&self.0)
545 }
546 }
547
548 #[test]
549 fn capture_or_extract_drops_empty_source_trace() {
550 let src = DiagSourceWithEmptyTrace(SpanTrace::capture());
553 assert!(!src.0.is_captured(), "precondition: source trace is empty");
554 let opt = <OptionalSpanTrace as crate::Capturable>::capture_or_extract(&src);
555 assert!(opt.is_none());
556 }
557
558 #[cfg(feature = "tracing")]
559 #[test]
560 fn span_trace_capture_or_extract_recaptures_over_empty_source_trace() {
561 let src = DiagSourceWithEmptyTrace(SpanTrace::capture());
563 assert!(!src.0.is_captured(), "precondition: source trace is empty");
564 let extracted = with_error_subscriber(|| {
567 let _g = tracing::info_span!("wrap_site").entered();
568 <SpanTrace as crate::Capturable>::capture_or_extract(&src)
569 });
570 assert!(extracted.is_captured());
571 }
572
573 #[cfg(feature = "tracing")]
574 #[test]
575 fn span_trace_capture_or_extract_keeps_captured_source_trace() {
576 let (src_trace, extracted) = with_error_subscriber(|| {
577 let src = DiagSourceWithEmptyTrace(leaf());
578 let t = <SpanTrace as crate::Capturable>::capture_or_extract(&src);
579 (src.0, t)
580 });
581 assert!(extracted.is_captured());
582 assert_eq!(extracted, src_trace);
583 }
584
585 #[cfg(not(feature = "tracing"))]
586 mod stub {
587 use super::*;
588
589 #[test]
590 fn capture_is_inert() {
591 assert!(!SpanTrace::capture().is_captured());
592 }
593
594 #[test]
595 fn display_is_empty() {
596 assert!(SpanTrace::capture().to_string().is_empty());
597 }
598
599 #[test]
600 fn equality_is_reflexive() {
601 let a = SpanTrace::capture();
602 assert_eq!(a, a.clone());
603 assert_eq!(SpanTrace::capture(), SpanTrace::capture());
604 }
605
606 #[test]
607 fn capture_or_extract_yields_stub() {
608 let extracted = <SpanTrace as crate::Capturable>::capture_or_extract(
609 &DiagSourceWithEmptyTrace(SpanTrace::capture()),
610 );
611 assert!(!extracted.is_captured());
612 }
613
614 #[test]
615 fn optional_capture_is_none() {
616 let opt: OptionalSpanTrace = Capturable::capture();
617 assert!(opt.is_none());
618 }
619 }
620}