fallow_cli/report/
sink.rs1use std::fmt;
18use std::io::{self, BufWriter, Write};
19use std::sync::Mutex;
20
21struct SinkInner {
22 file: Option<BufWriter<std::fs::File>>,
24 error: Option<io::Error>,
27 wrote: bool,
31}
32
33static SINK: Mutex<SinkInner> = Mutex::new(SinkInner {
34 file: None,
35 error: None,
36 wrote: false,
37});
38
39fn lock() -> std::sync::MutexGuard<'static, SinkInner> {
40 SINK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
41}
42
43pub fn set_file_sink(file: std::fs::File) {
46 let mut inner = lock();
47 inner.file = Some(BufWriter::new(file));
48 inner.error = None;
49 inner.wrote = false;
50}
51
52pub fn is_redirected() -> bool {
55 lock().file.is_some()
56}
57
58pub fn wrote() -> bool {
61 lock().wrote
62}
63
64pub fn flush() -> io::Result<()> {
67 let mut inner = lock();
68 if let Some(error) = inner.error.take() {
69 return Err(error);
70 }
71 match inner.file.as_mut() {
72 Some(writer) => writer.flush(),
73 None => Ok(()),
74 }
75}
76
77pub fn write_fmt_line(args: fmt::Arguments<'_>) {
80 let mut inner = lock();
81 if inner.error.is_some() {
82 return;
83 }
84 if inner.file.is_some() {
85 inner.wrote = true;
86 }
87 let result = match inner.file.as_mut() {
88 Some(writer) => writeln!(writer, "{args}"),
89 None => {
90 let _ = writeln!(io::stdout(), "{args}");
93 Ok(())
94 }
95 };
96 if let Err(error) = result {
97 inner.error = Some(error);
98 }
99}
100
101pub fn write_fmt_str(args: fmt::Arguments<'_>) {
103 let mut inner = lock();
104 if inner.error.is_some() {
105 return;
106 }
107 if inner.file.is_some() {
108 inner.wrote = true;
109 }
110 let result = match inner.file.as_mut() {
111 Some(writer) => write!(writer, "{args}"),
112 None => {
113 let _ = write!(io::stdout(), "{args}");
114 Ok(())
115 }
116 };
117 if let Err(error) = result {
118 inner.error = Some(error);
119 }
120}
121
122macro_rules! outln {
125 () => {
126 $crate::report::sink::write_fmt_line(::std::format_args!(""))
127 };
128 ($($arg:tt)*) => {
129 $crate::report::sink::write_fmt_line(::std::format_args!($($arg)*))
130 };
131}
132
133macro_rules! out {
136 ($($arg:tt)*) => {
137 $crate::report::sink::write_fmt_str(::std::format_args!($($arg)*))
138 };
139}
140
141pub(crate) use {out, outln};
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::io::Read;
147
148 static TEST_GUARD: Mutex<()> = Mutex::new(());
152
153 fn reset() {
154 let mut inner = lock();
155 inner.file = None;
156 inner.error = None;
157 }
158
159 #[test]
160 fn redirects_content_to_file_and_reports_flush_state() {
161 let _g = TEST_GUARD.lock().unwrap_or_else(|p| p.into_inner());
162 reset();
163 assert!(!is_redirected());
164
165 let dir = tempfile::tempdir().expect("tempdir");
166 let path = dir.path().join("out.txt");
167 let file = std::fs::File::create(&path).expect("create");
168 set_file_sink(file);
169 assert!(is_redirected());
170
171 outln!("line one");
172 out!("partial ");
173 outln!("end");
174 flush().expect("flush ok");
175
176 let mut contents = String::new();
177 std::fs::File::open(&path)
178 .expect("open")
179 .read_to_string(&mut contents)
180 .expect("read");
181 assert_eq!(contents, "line one\npartial end\n");
182 assert!(!contents.contains('\u{1b}'), "no ANSI escapes in file");
183
184 reset();
185 assert!(!is_redirected());
186 }
187
188 #[test]
189 fn flush_is_ok_when_writing_to_stdout() {
190 let _g = TEST_GUARD.lock().unwrap_or_else(|p| p.into_inner());
191 reset();
192 assert!(flush().is_ok());
193 }
194}