use crate::run::poll_until;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use tracing::Level;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::writer::MakeWriterExt;
use tracing_subscriber::fmt::{MakeWriter, TestWriter};
use tracing_subscriber::layer::SubscriberExt;
#[derive(Clone, Debug, Default)]
pub struct LogCapture(Arc<Mutex<Vec<u8>>>);
impl LogCapture {
pub fn new() -> LogCapture {
LogCapture::default()
}
pub fn lines(&self) -> Vec<String> {
String::from_utf8_lossy(&self.0.lock().expect("capture"))
.lines()
.map(str::to_string)
.collect()
}
pub fn wait_for<T>(
&self,
timeout: Duration,
what: &str,
check: impl FnMut() -> Option<T>,
) -> T {
poll_until(timeout, check).unwrap_or_else(|| {
panic!(
"timed out after {timeout:?} waiting for: {what}\n--- captured ---\n{}",
self.lines().join("\n")
)
})
}
pub fn wait_for_line(&self, timeout: Duration, needle: &str) -> String {
self.wait_for(timeout, &format!("a line containing {needle:?}"), || {
self.lines().into_iter().find(|l| l.contains(needle))
})
}
}
impl std::io::Write for LogCapture {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().expect("capture").extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for LogCapture {
type Writer = LogCapture;
fn make_writer(&'a self) -> LogCapture {
self.clone()
}
}
pub fn capture_logs(level: Level, f: impl FnOnce()) -> Vec<String> {
let capture = LogCapture::new();
under_subscriber(level, capture.clone().and(TestWriter::default()), f);
capture.lines()
}
pub fn show_logs(level: Level, f: impl FnOnce()) {
under_subscriber(level, TestWriter::default(), f);
}
static BYSTANDER: LazyLock<tracing::Dispatch> =
LazyLock::new(|| tracing::Dispatch::new(tracing_subscriber::registry().with(LevelFilter::OFF)));
fn under_subscriber<W>(level: Level, writer: W, f: impl FnOnce())
where
W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
let subscriber = tracing_subscriber::fmt()
.with_writer(writer)
.with_max_level(level)
.without_time()
.finish();
LazyLock::force(&BYSTANDER);
tracing::subscriber::with_default(subscriber, f);
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn wait_for_line_returns_the_matching_line() {
let mut capture = LogCapture::new();
capture.write_all(b"starting\nready at 1\n").unwrap();
assert_eq!(
capture.wait_for_line(Duration::from_secs(1), "ready"),
"ready at 1"
);
}
#[test]
#[should_panic(expected = "--- captured ---\nstarting")]
fn a_timeout_panics_with_the_capture() {
let mut capture = LogCapture::new();
capture.write_all(b"starting\n").unwrap();
capture.wait_for_line(Duration::from_millis(20), "ready");
}
fn hit(from: &str) {
tracing::warn!(from, "callsite hit");
}
fn warn_enabled() -> bool {
tracing::enabled!(Level::WARN)
}
#[test]
fn an_event_is_captured_after_a_bare_thread_reached_its_callsite_first() {
let lines = capture_logs(Level::WARN, || {
std::thread::spawn(|| hit("other")).join().unwrap();
hit("caller");
});
assert_eq!(lines.len(), 1, "{lines:?}");
assert!(lines[0].contains("caller"), "{lines:?}");
assert!(!lines[0].contains("other"), "{lines:?}");
}
#[test]
fn show_logs_enables_a_callsite_a_bare_thread_reached_first() {
let mut seen = None;
show_logs(Level::WARN, || {
assert!(!std::thread::spawn(warn_enabled).join().unwrap());
seen = Some(warn_enabled());
});
assert_eq!(seen, Some(true));
}
#[test]
fn the_bystander_enables_no_level() {
use tracing::Subscriber as _;
use tracing_subscriber::{Registry, layer::Layered};
let s = BYSTANDER
.downcast_ref::<Layered<LevelFilter, Registry>>()
.expect("bystander type");
assert_eq!(s.max_level_hint(), Some(LevelFilter::OFF));
}
}