use std::fmt;
use std::io::{self, BufWriter, Write};
use std::sync::Mutex;
struct SinkInner {
file: Option<BufWriter<std::fs::File>>,
error: Option<io::Error>,
wrote: bool,
}
static SINK: Mutex<SinkInner> = Mutex::new(SinkInner {
file: None,
error: None,
wrote: false,
});
fn lock() -> std::sync::MutexGuard<'static, SinkInner> {
SINK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn set_file_sink(file: std::fs::File) {
let mut inner = lock();
inner.file = Some(BufWriter::new(file));
inner.error = None;
inner.wrote = false;
}
pub fn is_redirected() -> bool {
lock().file.is_some()
}
pub fn wrote() -> bool {
lock().wrote
}
pub fn flush() -> io::Result<()> {
let mut inner = lock();
if let Some(error) = inner.error.take() {
return Err(error);
}
match inner.file.as_mut() {
Some(writer) => writer.flush(),
None => Ok(()),
}
}
pub fn write_fmt_line(args: fmt::Arguments<'_>) {
let mut inner = lock();
if inner.error.is_some() {
return;
}
if inner.file.is_some() {
inner.wrote = true;
}
let result = match inner.file.as_mut() {
Some(writer) => writeln!(writer, "{args}"),
None => {
let _ = writeln!(io::stdout(), "{args}");
Ok(())
}
};
if let Err(error) = result {
inner.error = Some(error);
}
}
pub fn write_fmt_str(args: fmt::Arguments<'_>) {
let mut inner = lock();
if inner.error.is_some() {
return;
}
if inner.file.is_some() {
inner.wrote = true;
}
let result = match inner.file.as_mut() {
Some(writer) => write!(writer, "{args}"),
None => {
let _ = write!(io::stdout(), "{args}");
Ok(())
}
};
if let Err(error) = result {
inner.error = Some(error);
}
}
macro_rules! outln {
() => {
$crate::report::sink::write_fmt_line(::std::format_args!(""))
};
($($arg:tt)*) => {
$crate::report::sink::write_fmt_line(::std::format_args!($($arg)*))
};
}
macro_rules! out {
($($arg:tt)*) => {
$crate::report::sink::write_fmt_str(::std::format_args!($($arg)*))
};
}
pub(crate) use {out, outln};
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
static TEST_GUARD: Mutex<()> = Mutex::new(());
fn reset() {
let mut inner = lock();
inner.file = None;
inner.error = None;
}
#[test]
fn redirects_content_to_file_and_reports_flush_state() {
let _g = TEST_GUARD.lock().unwrap_or_else(|p| p.into_inner());
reset();
assert!(!is_redirected());
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("out.txt");
let file = std::fs::File::create(&path).expect("create");
set_file_sink(file);
assert!(is_redirected());
outln!("line one");
out!("partial ");
outln!("end");
flush().expect("flush ok");
let mut contents = String::new();
std::fs::File::open(&path)
.expect("open")
.read_to_string(&mut contents)
.expect("read");
assert_eq!(contents, "line one\npartial end\n");
assert!(!contents.contains('\u{1b}'), "no ANSI escapes in file");
reset();
assert!(!is_redirected());
}
#[test]
fn flush_is_ok_when_writing_to_stdout() {
let _g = TEST_GUARD.lock().unwrap_or_else(|p| p.into_inner());
reset();
assert!(flush().is_ok());
}
}