candela_vm/
captured_output.rs1use std::cell::Cell;
16use std::cell::RefCell;
17use std::io::Write;
18
19thread_local! {
20 pub static CAPTURED_OUTPUT: RefCell<String> = const { RefCell::new(String::new()) };
21 static CAPTURING: Cell<bool> = const { Cell::new(false) };
22}
23
24#[inline]
26pub fn is_capturing() -> bool {
27 cfg!(target_arch = "wasm32") || CAPTURING.with(Cell::get)
28}
29
30pub fn set_capturing(on: bool) -> bool {
33 CAPTURING.replace(on)
34}
35
36pub fn print(s: &str) {
38 CAPTURED_OUTPUT.with(|o| o.borrow_mut().push_str(s));
39}
40
41pub fn stdout() -> OutputHandle {
43 if is_capturing() {
44 OutputHandle::Captured
45 } else {
46 OutputHandle::Stdout(std::io::stdout().lock())
47 }
48}
49
50pub fn stderr() -> OutputHandle {
52 if is_capturing() {
53 OutputHandle::Captured
54 } else {
55 OutputHandle::Stderr(std::io::stderr())
56 }
57}
58
59macro_rules! out {
67 ($handle:expr, $($arg:tt)*) => {{
68 let _ = std::io::Write::write_fmt(&mut $handle, format_args!($($arg)*));
69 }};
70}
71
72macro_rules! outln {
74 ($handle:expr, $($arg:tt)*) => {{
75 let _ = std::io::Write::write_fmt(
76 &mut $handle,
77 format_args!("{}\n", format_args!($($arg)*)),
78 );
79 }};
80}
81
82pub(crate) use out;
83pub(crate) use outln;
84
85pub enum OutputHandle {
88 Stdout(std::io::StdoutLock<'static>),
89 Stderr(std::io::Stderr),
90 Captured,
91}
92
93impl Write for OutputHandle {
94 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
95 match self {
96 Self::Stdout(out) => out.write(buf),
97 Self::Stderr(out) => out.write(buf),
98 Self::Captured => {
99 if let Ok(s) = std::str::from_utf8(buf) {
100 print(s);
101 }
102 Ok(buf.len())
103 }
104 }
105 }
106
107 fn flush(&mut self) -> std::io::Result<()> {
108 match self {
109 Self::Stdout(out) => out.flush(),
110 Self::Stderr(out) => out.flush(),
111 Self::Captured => Ok(()),
112 }
113 }
114
115 fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
116 match self {
117 Self::Stdout(out) => out.write_fmt(fmt),
118 Self::Stderr(out) => out.write_fmt(fmt),
119 Self::Captured => {
120 print(&fmt.to_string());
121 Ok(())
122 }
123 }
124 }
125}
126
127#[cfg(test)]
128mod output_tests {
129 use super::CAPTURED_OUTPUT;
130 use super::OutputHandle;
131 use std::io::Write;
132
133 struct Refuses {
136 attempts: u32,
137 }
138
139 impl Write for Refuses {
140 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
141 self.attempts += 1;
142 Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
143 }
144
145 fn flush(&mut self) -> std::io::Result<()> {
146 Ok(())
147 }
148 }
149
150 #[test]
151 fn a_refused_write_costs_the_text_and_nothing_else() {
152 let mut refuses = Refuses { attempts: 0 };
153
154 out!(refuses, "part of a line");
155 outln!(refuses, "a whole {}", "line");
156
157 assert_eq!(
160 refuses.attempts, 2,
161 "both writes reach the stream before their errors are dropped"
162 );
163 }
164
165 #[test]
166 fn a_write_that_lands_keeps_its_text() {
167 CAPTURED_OUTPUT.with(|o| o.borrow_mut().clear());
168
169 let mut handle = OutputHandle::Captured;
170 out!(handle, "{}:", "key");
171 outln!(handle, "{}", 42);
172
173 let captured = CAPTURED_OUTPUT.with(|o| o.borrow().clone());
174 assert_eq!(captured, "key:42\n");
175 }
176}