1use crate::run::poll_until;
8use std::sync::{Arc, LazyLock, Mutex};
9use std::time::Duration;
10use tracing::Level;
11use tracing_subscriber::filter::LevelFilter;
12use tracing_subscriber::fmt::writer::MakeWriterExt;
13use tracing_subscriber::fmt::{MakeWriter, TestWriter};
14use tracing_subscriber::layer::SubscriberExt;
15
16#[derive(Clone, Debug, Default)]
18pub struct LogCapture(Arc<Mutex<Vec<u8>>>);
19
20impl LogCapture {
21 pub fn new() -> LogCapture {
23 LogCapture::default()
24 }
25
26 pub fn lines(&self) -> Vec<String> {
30 String::from_utf8_lossy(&self.0.lock().expect("capture"))
31 .lines()
32 .map(str::to_string)
33 .collect()
34 }
35
36 pub fn wait_for<T>(
42 &self,
43 timeout: Duration,
44 what: &str,
45 check: impl FnMut() -> Option<T>,
46 ) -> T {
47 poll_until(timeout, check).unwrap_or_else(|| {
48 panic!(
49 "timed out after {timeout:?} waiting for: {what}\n--- captured ---\n{}",
50 self.lines().join("\n")
51 )
52 })
53 }
54
55 pub fn wait_for_line(&self, timeout: Duration, needle: &str) -> String {
60 self.wait_for(timeout, &format!("a line containing {needle:?}"), || {
61 self.lines().into_iter().find(|l| l.contains(needle))
62 })
63 }
64}
65
66impl std::io::Write for LogCapture {
67 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
68 self.0.lock().expect("capture").extend_from_slice(buf);
69 Ok(buf.len())
70 }
71
72 fn flush(&mut self) -> std::io::Result<()> {
73 Ok(())
74 }
75}
76
77impl<'a> MakeWriter<'a> for LogCapture {
78 type Writer = LogCapture;
79
80 fn make_writer(&'a self) -> LogCapture {
81 self.clone()
82 }
83}
84
85pub fn capture_logs(level: Level, f: impl FnOnce()) -> Vec<String> {
91 let capture = LogCapture::new();
92 under_subscriber(level, capture.clone().and(TestWriter::default()), f);
93 capture.lines()
94}
95
96pub fn show_logs(level: Level, f: impl FnOnce()) {
102 under_subscriber(level, TestWriter::default(), f);
103}
104
105static BYSTANDER: LazyLock<tracing::Dispatch> =
108 LazyLock::new(|| tracing::Dispatch::new(tracing_subscriber::registry().with(LevelFilter::OFF)));
109
110fn under_subscriber<W>(level: Level, writer: W, f: impl FnOnce())
111where
112 W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
113{
114 let subscriber = tracing_subscriber::fmt()
115 .with_writer(writer)
116 .with_max_level(level)
117 .without_time()
118 .finish();
119 LazyLock::force(&BYSTANDER);
120 tracing::subscriber::with_default(subscriber, f);
121}
122
123#[expect(
124 clippy::disallowed_methods,
125 reason = "each test joins its thread at once"
126)]
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use std::io::Write;
131
132 #[test]
133 fn wait_for_line_returns_the_matching_line() {
134 let mut capture = LogCapture::new();
135 capture.write_all(b"starting\nready at 1\n").unwrap();
136 assert_eq!(
137 capture.wait_for_line(Duration::from_secs(1), "ready"),
138 "ready at 1"
139 );
140 }
141
142 #[test]
144 #[should_panic(expected = "--- captured ---\nstarting")]
145 fn a_timeout_panics_with_the_capture() {
146 let mut capture = LogCapture::new();
147 capture.write_all(b"starting\n").unwrap();
148 capture.wait_for_line(Duration::from_millis(20), "ready");
149 }
150
151 fn hit(from: &str) {
152 tracing::warn!(from, "callsite hit");
153 }
154
155 fn warn_enabled() -> bool {
156 tracing::enabled!(Level::WARN)
157 }
158
159 #[test]
162 fn an_event_is_captured_after_a_bare_thread_reached_its_callsite_first() {
163 let lines = capture_logs(Level::WARN, || {
164 std::thread::spawn(|| hit("other")).join().unwrap();
165 hit("caller");
166 });
167 assert_eq!(lines.len(), 1, "{lines:?}");
168 assert!(lines[0].contains("caller"), "{lines:?}");
169 assert!(!lines[0].contains("other"), "{lines:?}");
170 }
171
172 #[test]
175 fn show_logs_enables_a_callsite_a_bare_thread_reached_first() {
176 let mut seen = None;
177 show_logs(Level::WARN, || {
178 assert!(!std::thread::spawn(warn_enabled).join().unwrap());
179 seen = Some(warn_enabled());
180 });
181 assert_eq!(seen, Some(true));
182 }
183
184 #[test]
186 fn the_bystander_enables_no_level() {
187 use tracing::Subscriber as _;
188 use tracing_subscriber::{Registry, layer::Layered};
189 let s = BYSTANDER
190 .downcast_ref::<Layered<LevelFilter, Registry>>()
191 .expect("bystander type");
192 assert_eq!(s.max_level_hint(), Some(LevelFilter::OFF));
193 }
194}