use std::io;
use std::sync::{Arc, Mutex};
use tracing::subscriber::DefaultGuard;
use tracing_subscriber::fmt::MakeWriter;
pub struct LogCapture {
buffer: Arc<Mutex<Vec<u8>>>,
_guard: DefaultGuard,
}
impl LogCapture {
pub fn new() -> Self {
let buffer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let make_writer = SharedBufferMakeWriter(Arc::clone(&buffer));
let subscriber = tracing_subscriber::fmt()
.with_writer(make_writer)
.with_max_level(tracing::Level::TRACE)
.with_ansi(false)
.without_time()
.finish();
let guard = tracing::subscriber::set_default(subscriber);
Self {
buffer,
_guard: guard,
}
}
pub fn contents(&self) -> String {
let bytes = self.buffer.lock().expect("log capture mutex poisoned");
String::from_utf8(bytes.clone()).expect("captured tracing output must be UTF-8")
}
pub fn assert_does_not_contain(&self, needle: &str) {
let contents = self.contents();
assert!(
!contents.contains(needle),
"captured tracing output must not contain {needle:?}; got:\n{contents}"
);
}
pub fn assert_contains(&self, needle: &str) {
let contents = self.contents();
assert!(
contents.contains(needle),
"captured tracing output must contain {needle:?}; got:\n{contents}"
);
}
}
impl Default for LogCapture {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
struct SharedBufferMakeWriter(Arc<Mutex<Vec<u8>>>);
impl<'a> MakeWriter<'a> for SharedBufferMakeWriter {
type Writer = SharedBufferWriter;
fn make_writer(&'a self) -> Self::Writer {
SharedBufferWriter(Arc::clone(&self.0))
}
}
struct SharedBufferWriter(Arc<Mutex<Vec<u8>>>);
impl io::Write for SharedBufferWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut guard = self
.0
.lock()
.map_err(|_| io::Error::other("log capture mutex poisoned"))?;
guard.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}