term_transcript/test/
utils.rs

1use std::{
2    io::{self, IsTerminal, Write},
3    str,
4};
5
6use termcolor::{Ansi, ColorChoice, ColorSpec, NoColor, StandardStream, WriteColor};
7
8#[cfg(test)]
9use self::tests::print_to_buffer;
10
11// Patch `print!` / `println!` macros for testing similarly to how they are patched in `std`.
12#[cfg(test)]
13macro_rules! print {
14    ($($arg:tt)*) => (print_to_buffer(std::format_args!($($arg)*)));
15}
16#[cfg(test)]
17macro_rules! println {
18    ($($arg:tt)*) => {
19        print_to_buffer(std::format_args!($($arg)*));
20        print_to_buffer(std::format_args!("\n"));
21    }
22}
23
24/// Writer that adds `padding` to each printed line.
25#[derive(Debug)]
26pub(super) struct IndentingWriter<W> {
27    inner: W,
28    padding: &'static [u8],
29    new_line: bool,
30}
31
32impl<W: Write> IndentingWriter<W> {
33    pub fn new(writer: W, padding: &'static [u8]) -> Self {
34        Self {
35            inner: writer,
36            padding,
37            new_line: true,
38        }
39    }
40}
41
42impl<W: Write> Write for IndentingWriter<W> {
43    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
44        for (i, line) in buf.split(|&c| c == b'\n').enumerate() {
45            if i > 0 {
46                self.inner.write_all(b"\n")?;
47            }
48            if !line.is_empty() && (i > 0 || self.new_line) {
49                self.inner.write_all(self.padding)?;
50            }
51            self.inner.write_all(line)?;
52        }
53        self.new_line = buf.ends_with(b"\n");
54        Ok(buf.len())
55    }
56
57    fn flush(&mut self) -> io::Result<()> {
58        self.inner.flush()
59    }
60}
61
62/// `Write`r that uses `print!` / `println!` for output.
63///
64/// # Why is this needed?
65///
66/// This writer is used to output text within `TestConfig`. The primary use case of
67/// `TestConfig` is to be used within tests, and there the output is captured by default,
68/// which is implemented by effectively overriding the `std::print*` family of macros
69/// (see `std::io::_print()` for details). Using `termcolor::StandardStream` or another `Write`r
70/// connected to stdout will lead to `TestConfig` output not being captured,
71/// resulting in weird / incomprehensible test output.
72///
73/// This issue is solved by using a writer that uses `std::print*` macros internally,
74/// instead of (implicitly) binding to `std::io::stdout()`.
75#[derive(Debug, Default)]
76pub(super) struct PrintlnWriter {
77    line_buffer: Vec<u8>,
78}
79
80impl Write for PrintlnWriter {
81    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
82        for (i, line) in buf.split(|&c| c == b'\n').enumerate() {
83            if i > 0 {
84                // Output previously saved line and clear the line buffer.
85                let str = str::from_utf8(&self.line_buffer)
86                    .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
87                println!("{str}");
88                self.line_buffer.clear();
89            }
90            self.line_buffer.extend_from_slice(line);
91        }
92        Ok(buf.len())
93    }
94
95    fn flush(&mut self) -> io::Result<()> {
96        let str = str::from_utf8(&self.line_buffer)
97            .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
98        print!("{str}");
99        self.line_buffer.clear();
100        Ok(())
101    }
102}
103
104/// `PrintlnWriter` extension with ANSI color support.
105pub(super) enum ColorPrintlnWriter {
106    NoColor(NoColor<PrintlnWriter>),
107    Ansi(Ansi<PrintlnWriter>),
108}
109
110impl ColorPrintlnWriter {
111    pub fn new(color_choice: ColorChoice) -> Self {
112        let is_ansi = match color_choice {
113            ColorChoice::Never => false,
114            ColorChoice::Always | ColorChoice::AlwaysAnsi => true,
115            ColorChoice::Auto => {
116                if io::stdout().is_terminal() {
117                    StandardStream::stdout(color_choice).supports_color()
118                } else {
119                    false
120                }
121            }
122        };
123
124        let inner = PrintlnWriter::default();
125        if is_ansi {
126            Self::Ansi(Ansi::new(inner))
127        } else {
128            Self::NoColor(NoColor::new(inner))
129        }
130    }
131}
132
133impl Write for ColorPrintlnWriter {
134    #[inline]
135    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
136        match self {
137            Self::Ansi(ansi) => ansi.write(buf),
138            Self::NoColor(no_color) => no_color.write(buf),
139        }
140    }
141
142    #[inline]
143    fn flush(&mut self) -> io::Result<()> {
144        match self {
145            Self::Ansi(ansi) => ansi.flush(),
146            Self::NoColor(no_color) => no_color.flush(),
147        }
148    }
149}
150
151impl WriteColor for ColorPrintlnWriter {
152    #[inline]
153    fn supports_color(&self) -> bool {
154        match self {
155            Self::Ansi(ansi) => ansi.supports_color(),
156            Self::NoColor(no_color) => no_color.supports_color(),
157        }
158    }
159
160    #[inline]
161    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
162        match self {
163            Self::Ansi(ansi) => ansi.set_color(spec),
164            Self::NoColor(no_color) => no_color.set_color(spec),
165        }
166    }
167
168    #[inline]
169    fn reset(&mut self) -> io::Result<()> {
170        match self {
171            Self::Ansi(ansi) => ansi.reset(),
172            Self::NoColor(no_color) => no_color.reset(),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use std::{cell::RefCell, fmt, mem};
180
181    use super::*;
182
183    thread_local! {
184        static OUTPUT_CAPTURE: RefCell<Vec<u8>> = RefCell::default();
185    }
186
187    pub fn print_to_buffer(args: fmt::Arguments<'_>) {
188        OUTPUT_CAPTURE.with(|capture| {
189            let mut lock = capture.borrow_mut();
190            lock.write_fmt(args).ok();
191        });
192    }
193
194    #[test]
195    fn indenting_writer_basics() -> io::Result<()> {
196        let mut buffer = vec![];
197        let mut writer = IndentingWriter::new(&mut buffer, b"  ");
198        write!(writer, "Hello, ")?;
199        writeln!(writer, "world!")?;
200        writeln!(writer, "many\n  lines!")?;
201
202        assert_eq!(buffer, b"  Hello, world!\n  many\n    lines!\n" as &[u8]);
203        Ok(())
204    }
205
206    #[test]
207    fn println_writer_basics() -> io::Result<()> {
208        let mut writer = PrintlnWriter::default();
209        write!(writer, "Hello, ")?;
210        writeln!(writer, "world!")?;
211        writeln!(writer, "many\n  lines!")?;
212
213        let captured = OUTPUT_CAPTURE.with(|capture| {
214            let mut lock = capture.borrow_mut();
215            mem::take(&mut *lock)
216        });
217
218        assert_eq!(captured, b"Hello, world!\nmany\n  lines!\n");
219        Ok(())
220    }
221}