use std::io;
use std::sync::{Arc, Mutex, MutexGuard};
#[derive(Clone, Default)]
pub struct BufferWriter(Arc<Mutex<Vec<u8>>>);
impl BufferWriter {
pub fn captured(&self) -> String {
String::from_utf8_lossy(&self.0.lock().unwrap_or_else(|e| e.into_inner())).to_string()
}
}
impl io::Write for BufferWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for BufferWriter {
type Writer = BufferWriterGuard<'a>;
fn make_writer(&'a self) -> Self::Writer {
BufferWriterGuard(self.0.lock().unwrap_or_else(|e| e.into_inner()))
}
}
pub struct BufferWriterGuard<'a>(MutexGuard<'a, Vec<u8>>);
impl io::Write for BufferWriterGuard<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub fn capture_tracing_warnings<F: FnOnce()>(body: F) -> String {
let buf = BufferWriter::default();
let subscriber = tracing_subscriber::fmt()
.with_writer(buf.clone())
.with_max_level(tracing::Level::WARN)
.without_time()
.with_ansi(false)
.finish();
tracing::subscriber::with_default(subscriber, body);
buf.captured()
}
#[cfg(test)]
mod tests {
use super::super::test_sources;
use std::path::{Path, PathBuf};
fn every_source() -> Vec<PathBuf> {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut all = test_sources::rust_sources(&src);
all.extend(test_sources::test_sources(&src));
all.retain(|p| !p.ends_with("tracing_capture.rs"));
all
}
fn capture_call_sites(src: &str) -> Vec<(String, bool)> {
let lines: Vec<&str> = src.lines().collect();
let mut sites = Vec::new();
let mut current: Option<usize> = None;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("fn ")
|| trimmed.starts_with("pub fn ")
|| trimmed.starts_with("pub(crate) fn ")
|| trimmed.starts_with("pub(super) fn ")
{
current = Some(i);
}
if !line.contains("capture_tracing_warnings(") || trimmed.starts_with("///") {
continue;
}
let Some(fn_line) = current else { continue };
let name = lines[fn_line]
.trim_start()
.trim_start_matches("pub(crate) ")
.trim_start_matches("pub(super) ")
.trim_start_matches("pub ")
.trim_start_matches("fn ")
.split('(')
.next()
.unwrap_or_default()
.to_string();
if name == "capture_tracing_warnings" {
continue;
}
let mut attrs = String::new();
for prev in lines[..fn_line].iter().rev() {
let p = prev.trim_start();
if p.starts_with('#') || p.starts_with("//") {
attrs.push_str(p);
attrs.push('\n');
} else {
break;
}
}
if !attrs.contains("#[test]") {
continue;
}
sites.push((name, attrs.contains("serial(tracing)")));
}
sites.sort();
sites.dedup();
sites
}
#[test]
fn every_tracing_capture_test_takes_the_serial_key() {
let mut unmarked = Vec::new();
let mut total = 0usize;
for path in every_source() {
let src = std::fs::read_to_string(&path).expect("read source");
for (name, serial) in capture_call_sites(&src) {
total += 1;
if !serial {
unmarked.push(format!("{}::{name}", path.display()));
}
}
}
assert!(
total >= 8,
"the walk found only {total} capture tests; it stopped seeing the population"
);
assert!(
unmarked.is_empty(),
"a tracing subscriber is installed thread-locally but primes a \
process-global level hint, so every capture test needs \
#[serial_test::serial(tracing)]; missing on {unmarked:?}"
);
}
#[test]
fn a_capture_test_without_the_serial_key_is_reported() {
let src = "\
#[test]
fn marked() {
let out = capture_tracing_warnings(|| {});
}
#[test]
fn unmarked() {
let out = capture_tracing_warnings(|| {});
}
";
let src = src.replace(
"#[test]\nfn marked()",
"#[test]\n#[serial_test::serial(tracing)]\nfn marked()",
);
assert_eq!(
capture_call_sites(&src),
vec![
("marked".to_string(), true),
("unmarked".to_string(), false)
]
);
}
}