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#[cfg(test)]
124mod tests {
125 use super::*;
126 use std::io::Write;
127
128 #[test]
129 fn wait_for_line_returns_the_matching_line() {
130 let mut capture = LogCapture::new();
131 capture.write_all(b"starting\nready at 1\n").unwrap();
132 assert_eq!(
133 capture.wait_for_line(Duration::from_secs(1), "ready"),
134 "ready at 1"
135 );
136 }
137
138 #[test]
140 #[should_panic(expected = "--- captured ---\nstarting")]
141 fn a_timeout_panics_with_the_capture() {
142 let mut capture = LogCapture::new();
143 capture.write_all(b"starting\n").unwrap();
144 capture.wait_for_line(Duration::from_millis(20), "ready");
145 }
146
147 fn hit(from: &str) {
148 tracing::warn!(from, "callsite hit");
149 }
150
151 fn warn_enabled() -> bool {
152 tracing::enabled!(Level::WARN)
153 }
154
155 #[test]
158 fn an_event_is_captured_after_a_bare_thread_reached_its_callsite_first() {
159 let lines = capture_logs(Level::WARN, || {
160 std::thread::spawn(|| hit("other")).join().unwrap();
161 hit("caller");
162 });
163 assert_eq!(lines.len(), 1, "{lines:?}");
164 assert!(lines[0].contains("caller"), "{lines:?}");
165 assert!(!lines[0].contains("other"), "{lines:?}");
166 }
167
168 #[test]
171 fn show_logs_enables_a_callsite_a_bare_thread_reached_first() {
172 let mut seen = None;
173 show_logs(Level::WARN, || {
174 assert!(!std::thread::spawn(warn_enabled).join().unwrap());
175 seen = Some(warn_enabled());
176 });
177 assert_eq!(seen, Some(true));
178 }
179
180 #[test]
182 fn the_bystander_enables_no_level() {
183 use tracing::Subscriber as _;
184 use tracing_subscriber::{Registry, layer::Layered};
185 let s = BYSTANDER
186 .downcast_ref::<Layered<LevelFilter, Registry>>()
187 .expect("bystander type");
188 assert_eq!(s.max_level_hint(), Some(LevelFilter::OFF));
189 }
190}