Skip to main content

max / synckit

5.4 KB · 157 lines History Blame Raw
1 //! Test-only helpers shared across the crate's unit tests.
2 //!
3 //! Compiled only under `cfg(test)`, so nothing here reaches a consumer.
4
5 /// One `tracing` event a [`events_from`] run saw.
6 pub(crate) struct CapturedEvent {
7 /// The event's message, which `tracing` carries as a field named `message`.
8 pub(crate) message: Option<String>,
9 /// Every other field, in the order they were recorded.
10 pub(crate) fields: Vec<(String, String)>,
11 }
12
13 impl CapturedEvent {
14 /// The value recorded for `name`, if the event carried it.
15 pub(crate) fn field(&self, name: &str) -> Option<&str> {
16 self.fields
17 .iter()
18 .find(|(k, _)| k == name)
19 .map(|(_, v)| v.as_str())
20 }
21 }
22
23 /// Collects every field of one event.
24 ///
25 /// A `%value` field arrives through `record_debug` as a `format_args`, whose
26 /// `Debug` rendering is the displayed text with no quotes around it, so both
27 /// recorders below produce the bare string.
28 #[derive(Default)]
29 struct EventVisitor {
30 message: Option<String>,
31 fields: Vec<(String, String)>,
32 }
33
34 impl EventVisitor {
35 fn put(&mut self, name: &str, value: String) {
36 if name == "message" {
37 self.message = Some(value);
38 } else {
39 self.fields.push((name.to_string(), value));
40 }
41 }
42 }
43
44 impl tracing::field::Visit for EventVisitor {
45 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
46 self.put(field.name(), format!("{value:?}"));
47 }
48
49 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
50 self.put(field.name(), value.to_string());
51 }
52
53 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
54 self.put(field.name(), value.to_string());
55 }
56 }
57
58 /// The buffer one [`events_from`] call is filling, and the thread it belongs to.
59 struct Sink {
60 thread: std::thread::ThreadId,
61 events: Vec<CapturedEvent>,
62 }
63
64 /// Where the installed subscriber puts what it sees. `None` between captures.
65 static SINK: std::sync::Mutex<Option<Sink>> = std::sync::Mutex::new(None);
66
67 /// One capture at a time, since there is one sink.
68 static CAPTURING: std::sync::Mutex<()> = std::sync::Mutex::new(());
69
70 /// The process-wide subscriber, installed once and never removed.
71 ///
72 /// Global rather than thread-scoped, which is the opposite of what it looks
73 /// like it should be. `tracing::subscriber::with_default` is thread-local, but
74 /// the interest cache it feeds is process-wide and is rebuilt whenever any
75 /// dispatcher is registered or dropped. Under a test harness running dozens of
76 /// threads, another test dropping its dispatcher can reset a callsite to
77 /// "never" between this one registering and emitting, and the event is silently
78 /// lost. Measured: passes on an 8-core box, fails on a 96-core one.
79 ///
80 /// Installing once and leaving it there makes the cached interest stable. The
81 /// sink is what moves, and it records only the thread that installed it, so a
82 /// capture still sees exactly its own events.
83 struct Recorder;
84
85 impl tracing::Subscriber for Recorder {
86 fn register_callsite(
87 &self,
88 _: &'static tracing::Metadata<'static>,
89 ) -> tracing::subscriber::Interest {
90 // `sometimes` so `enabled` is consulted per event rather than the
91 // verdict being cached; the sink comes and goes.
92 tracing::subscriber::Interest::sometimes()
93 }
94
95 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
96 true
97 }
98
99 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
100 tracing::span::Id::from_u64(1)
101 }
102
103 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
104
105 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
106
107 fn event(&self, event: &tracing::Event<'_>) {
108 let mut sink = SINK
109 .lock()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 let Some(sink) = sink.as_mut() else { return };
112 if sink.thread != std::thread::current().id() {
113 return;
114 }
115 let mut visitor = EventVisitor::default();
116 event.record(&mut visitor);
117 sink.events.push(CapturedEvent {
118 message: visitor.message,
119 fields: visitor.fields,
120 });
121 }
122
123 fn enter(&self, _: &tracing::span::Id) {}
124
125 fn exit(&self, _: &tracing::span::Id) {}
126 }
127
128 /// Every `tracing` event `f` emitted on this thread, in order.
129 ///
130 /// What it is for: a branch whose only effect is a log line, which no assertion
131 /// on a return value can reach.
132 pub(crate) fn events_from(f: impl FnOnce()) -> Vec<CapturedEvent> {
133 static INSTALL: std::sync::Once = std::sync::Once::new();
134 INSTALL.call_once(|| {
135 // Err would mean something else claimed the global default; nothing in
136 // this crate's tests does, and a capture that recorded nothing would
137 // fail its own assertions rather than pass quietly.
138 let _ = tracing::subscriber::set_global_default(Recorder);
139 });
140
141 let _capturing = CAPTURING
142 .lock()
143 .unwrap_or_else(std::sync::PoisonError::into_inner);
144 *SINK
145 .lock()
146 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Sink {
147 thread: std::thread::current().id(),
148 events: Vec::new(),
149 });
150 f();
151 SINK.lock()
152 .unwrap_or_else(std::sync::PoisonError::into_inner)
153 .take()
154 .expect("the sink this call installed")
155 .events
156 }
157