use std::io;
use std::sync::{Arc, Mutex};
use tracing::Level;
use tracing::subscriber::NoSubscriber;
#[derive(Clone, Default)]
pub(crate) struct CapturedLogs(Arc<Mutex<Vec<u8>>>);
impl CapturedLogs {
pub(crate) fn contents(&self) -> String {
let bytes = self
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
String::from_utf8_lossy(&bytes).into_owned()
}
}
impl io::Write for CapturedLogs {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub(crate) fn capture_at<T>(level: Level, f: impl FnOnce() -> T) -> (T, String) {
let logs = CapturedLogs::default();
let writer = logs.clone();
let capturing = tracing_subscriber::fmt()
.with_max_level(level)
.with_ansi(false)
.with_writer(move || writer.clone())
.finish();
let _second_dispatcher = tracing::Dispatch::new(NoSubscriber::default());
let value = tracing::subscriber::with_default(capturing, f);
drop(_second_dispatcher);
(value, logs.contents())
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use std::sync::Barrier;
use std::thread;
const DIAGNOSTIC: &str = "error at line 1";
#[inline(never)]
fn emit() {
tracing::warn!("{DIAGNOSTIC}");
}
#[test]
#[ignore = "run in a clean process by a_first_hit_from_a_foreign_thread_does_not_empty_the_capture"]
fn foreign_first_hit_inner() {
let barrier = Arc::new(Barrier::new(2));
let other = {
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait(); emit();
barrier.wait(); })
};
let ((), logs) = capture_at(Level::WARN, || {
barrier.wait();
barrier.wait();
emit();
});
other.join().expect("the foreign thread does not panic");
assert!(
logs.contains(DIAGNOSTIC),
"the event this subscriber wanted was dropped; captured: {logs:?}"
);
}
#[test]
fn a_first_hit_from_a_foreign_thread_does_not_empty_the_capture() {
let exe = std::env::current_exe().expect("the test binary knows its own path");
let module = module_path!()
.split_once("::")
.map_or(module_path!(), |(_, rest)| rest);
let name = format!("{module}::foreign_first_hit_inner");
let output = Command::new(exe)
.args(["--exact", &name, "--ignored", "--test-threads=1"])
.output()
.expect("the test binary can be re-run");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("test result: ok. 1 passed"),
"the clean-process run did not pass exactly one test:\n{stdout}\n{stderr}"
);
assert!(
output.status.success(),
"the clean-process run failed:\n{stdout}\n{stderr}"
);
}
}