1 | use std::{io, io::prelude::Write}; |
2 | |
3 | use super::OutputFormatter; |
4 | use crate::{ |
5 | bench::fmt_bench_samples, |
6 | console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, |
7 | term, |
8 | test_result::TestResult, |
9 | time, |
10 | types::NamePadding, |
11 | types::TestDesc, |
12 | }; |
13 | |
14 | // We insert a '\n' when the output hits 100 columns in quiet mode. 88 test |
15 | // result chars leaves 12 chars for a progress count like " 11704/12853". |
16 | const QUIET_MODE_MAX_COLUMN: usize = 88; |
17 | |
18 | pub(crate) struct TerseFormatter<T> { |
19 | out: OutputLocation<T>, |
20 | use_color: bool, |
21 | is_multithreaded: bool, |
22 | /// Number of columns to fill when aligning names |
23 | max_name_len: usize, |
24 | |
25 | test_count: usize, |
26 | test_column: usize, |
27 | total_test_count: usize, |
28 | } |
29 | |
30 | impl<T: Write> TerseFormatter<T> { |
31 | pub fn new( |
32 | out: OutputLocation<T>, |
33 | use_color: bool, |
34 | max_name_len: usize, |
35 | is_multithreaded: bool, |
36 | ) -> Self { |
37 | TerseFormatter { |
38 | out, |
39 | use_color, |
40 | max_name_len, |
41 | is_multithreaded, |
42 | test_count: 0, |
43 | test_column: 0, |
44 | total_test_count: 0, // initialized later, when write_run_start is called |
45 | } |
46 | } |
47 | |
48 | pub fn write_ok(&mut self) -> io::Result<()> { |
49 | self.write_short_result("." , term::color::GREEN) |
50 | } |
51 | |
52 | pub fn write_failed(&mut self, name: &str) -> io::Result<()> { |
53 | // Put failed tests on their own line and include the test name, so that it's faster |
54 | // to see which test failed without having to wait for them all to run. |
55 | |
56 | // normally, we write the progress unconditionally, even if the previous line was cut short. |
57 | // but if this is the very first column, no short results will have been printed and we'll end up with *only* the progress on the line. |
58 | // avoid this. |
59 | if self.test_column != 0 { |
60 | self.write_progress()?; |
61 | } |
62 | self.test_count += 1; |
63 | self.write_plain(format!("{name} --- " ))?; |
64 | self.write_pretty("FAILED" , term::color::RED)?; |
65 | self.write_plain(" \n" ) |
66 | } |
67 | |
68 | pub fn write_ignored(&mut self) -> io::Result<()> { |
69 | self.write_short_result("i" , term::color::YELLOW) |
70 | } |
71 | |
72 | pub fn write_bench(&mut self) -> io::Result<()> { |
73 | self.write_pretty("bench" , term::color::CYAN) |
74 | } |
75 | |
76 | pub fn write_short_result( |
77 | &mut self, |
78 | result: &str, |
79 | color: term::color::Color, |
80 | ) -> io::Result<()> { |
81 | self.write_pretty(result, color)?; |
82 | self.test_count += 1; |
83 | self.test_column += 1; |
84 | if self.test_column % QUIET_MODE_MAX_COLUMN == QUIET_MODE_MAX_COLUMN - 1 { |
85 | // We insert a new line regularly in order to flush the |
86 | // screen when dealing with line-buffered output (e.g., piping to |
87 | // `stamp` in the Rust CI). |
88 | self.write_progress()?; |
89 | } |
90 | |
91 | Ok(()) |
92 | } |
93 | |
94 | fn write_progress(&mut self) -> io::Result<()> { |
95 | let out = format!(" {}/{} \n" , self.test_count, self.total_test_count); |
96 | self.write_plain(out)?; |
97 | self.test_column = 0; |
98 | Ok(()) |
99 | } |
100 | |
101 | pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { |
102 | match self.out { |
103 | OutputLocation::Pretty(ref mut term) => { |
104 | if self.use_color { |
105 | term.fg(color)?; |
106 | } |
107 | term.write_all(word.as_bytes())?; |
108 | if self.use_color { |
109 | term.reset()?; |
110 | } |
111 | term.flush() |
112 | } |
113 | OutputLocation::Raw(ref mut stdout) => { |
114 | stdout.write_all(word.as_bytes())?; |
115 | stdout.flush() |
116 | } |
117 | } |
118 | } |
119 | |
120 | pub fn write_plain<S: AsRef<str>>(&mut self, s: S) -> io::Result<()> { |
121 | let s = s.as_ref(); |
122 | self.out.write_all(s.as_bytes())?; |
123 | self.out.flush() |
124 | } |
125 | |
126 | pub fn write_outputs(&mut self, state: &ConsoleTestState) -> io::Result<()> { |
127 | self.write_plain(" \nsuccesses: \n" )?; |
128 | let mut successes = Vec::new(); |
129 | let mut stdouts = String::new(); |
130 | for (f, stdout) in &state.not_failures { |
131 | successes.push(f.name.to_string()); |
132 | if !stdout.is_empty() { |
133 | stdouts.push_str(&format!("---- {} stdout ---- \n" , f.name)); |
134 | let output = String::from_utf8_lossy(stdout); |
135 | stdouts.push_str(&output); |
136 | stdouts.push(' \n' ); |
137 | } |
138 | } |
139 | if !stdouts.is_empty() { |
140 | self.write_plain(" \n" )?; |
141 | self.write_plain(&stdouts)?; |
142 | } |
143 | |
144 | self.write_plain(" \nsuccesses: \n" )?; |
145 | successes.sort(); |
146 | for name in &successes { |
147 | self.write_plain(&format!(" {name} \n" ))?; |
148 | } |
149 | Ok(()) |
150 | } |
151 | |
152 | pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { |
153 | self.write_plain(" \nfailures: \n" )?; |
154 | let mut failures = Vec::new(); |
155 | let mut fail_out = String::new(); |
156 | for (f, stdout) in &state.failures { |
157 | failures.push(f.name.to_string()); |
158 | if !stdout.is_empty() { |
159 | fail_out.push_str(&format!("---- {} stdout ---- \n" , f.name)); |
160 | let output = String::from_utf8_lossy(stdout); |
161 | fail_out.push_str(&output); |
162 | fail_out.push(' \n' ); |
163 | } |
164 | } |
165 | if !fail_out.is_empty() { |
166 | self.write_plain(" \n" )?; |
167 | self.write_plain(&fail_out)?; |
168 | } |
169 | |
170 | self.write_plain(" \nfailures: \n" )?; |
171 | failures.sort(); |
172 | for name in &failures { |
173 | self.write_plain(&format!(" {name} \n" ))?; |
174 | } |
175 | Ok(()) |
176 | } |
177 | |
178 | fn write_test_name(&mut self, desc: &TestDesc) -> io::Result<()> { |
179 | let name = desc.padded_name(self.max_name_len, desc.name.padding()); |
180 | if let Some(test_mode) = desc.test_mode() { |
181 | self.write_plain(format!("test {name} - {test_mode} ... " ))?; |
182 | } else { |
183 | self.write_plain(format!("test {name} ... " ))?; |
184 | } |
185 | |
186 | Ok(()) |
187 | } |
188 | } |
189 | |
190 | impl<T: Write> OutputFormatter for TerseFormatter<T> { |
191 | fn write_discovery_start(&mut self) -> io::Result<()> { |
192 | Ok(()) |
193 | } |
194 | |
195 | fn write_test_discovered(&mut self, desc: &TestDesc, test_type: &str) -> io::Result<()> { |
196 | self.write_plain(format!("{}: {test_type} \n" , desc.name)) |
197 | } |
198 | |
199 | fn write_discovery_finish(&mut self, _state: &ConsoleTestDiscoveryState) -> io::Result<()> { |
200 | Ok(()) |
201 | } |
202 | |
203 | fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> { |
204 | self.total_test_count = test_count; |
205 | let noun = if test_count != 1 { "tests" } else { "test" }; |
206 | let shuffle_seed_msg = if let Some(shuffle_seed) = shuffle_seed { |
207 | format!(" (shuffle seed: {shuffle_seed})" ) |
208 | } else { |
209 | String::new() |
210 | }; |
211 | self.write_plain(format!(" \nrunning {test_count} {noun}{shuffle_seed_msg} \n" )) |
212 | } |
213 | |
214 | fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { |
215 | // Remnants from old libtest code that used the padding value |
216 | // in order to indicate benchmarks. |
217 | // When running benchmarks, terse-mode should still print their name as if |
218 | // it is the Pretty formatter. |
219 | if !self.is_multithreaded && desc.name.padding() == NamePadding::PadOnRight { |
220 | self.write_test_name(desc)?; |
221 | } |
222 | |
223 | Ok(()) |
224 | } |
225 | |
226 | fn write_result( |
227 | &mut self, |
228 | desc: &TestDesc, |
229 | result: &TestResult, |
230 | _: Option<&time::TestExecTime>, |
231 | _: &[u8], |
232 | _: &ConsoleTestState, |
233 | ) -> io::Result<()> { |
234 | match *result { |
235 | TestResult::TrOk => self.write_ok(), |
236 | TestResult::TrFailed | TestResult::TrFailedMsg(_) | TestResult::TrTimedFail => { |
237 | self.write_failed(desc.name.as_slice()) |
238 | } |
239 | TestResult::TrIgnored => self.write_ignored(), |
240 | TestResult::TrBench(ref bs) => { |
241 | if self.is_multithreaded { |
242 | self.write_test_name(desc)?; |
243 | } |
244 | self.write_bench()?; |
245 | self.write_plain(format!(": {} \n" , fmt_bench_samples(bs))) |
246 | } |
247 | } |
248 | } |
249 | |
250 | fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { |
251 | self.write_plain(format!( |
252 | "test {} has been running for over {} seconds \n" , |
253 | desc.name, |
254 | time::TEST_WARN_TIMEOUT_S |
255 | )) |
256 | } |
257 | |
258 | fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> { |
259 | if state.options.display_output { |
260 | self.write_outputs(state)?; |
261 | } |
262 | let success = state.failed == 0; |
263 | if !success { |
264 | self.write_failures(state)?; |
265 | } |
266 | |
267 | self.write_plain(" \ntest result: " )?; |
268 | |
269 | if success { |
270 | // There's no parallelism at this point so it's safe to use color |
271 | self.write_pretty("ok" , term::color::GREEN)?; |
272 | } else { |
273 | self.write_pretty("FAILED" , term::color::RED)?; |
274 | } |
275 | |
276 | let s = format!( |
277 | ". {} passed; {} failed; {} ignored; {} measured; {} filtered out" , |
278 | state.passed, state.failed, state.ignored, state.measured, state.filtered_out |
279 | ); |
280 | |
281 | self.write_plain(s)?; |
282 | |
283 | if let Some(ref exec_time) = state.exec_time { |
284 | let time_str = format!("; finished in {exec_time}" ); |
285 | self.write_plain(time_str)?; |
286 | } |
287 | |
288 | self.write_plain(" \n\n" )?; |
289 | |
290 | // Custom handling of cases where there is only 1 test to execute and that test was ignored. |
291 | // We want to show more detailed information(why was the test ignored) for investigation purposes. |
292 | if self.total_test_count == 1 && state.ignores.len() == 1 { |
293 | let test_desc = &state.ignores[0].0; |
294 | if let Some(im) = test_desc.ignore_message { |
295 | self.write_plain(format!("test: {}, ignore_message: {} \n\n" , test_desc.name, im))?; |
296 | } |
297 | } |
298 | |
299 | Ok(success) |
300 | } |
301 | } |
302 | |