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::TestDesc, |
11 | }; |
12 | |
13 | pub(crate) struct PrettyFormatter<T> { |
14 | out: OutputLocation<T>, |
15 | use_color: bool, |
16 | time_options: Option<time::TestTimeOptions>, |
17 | |
18 | /// Number of columns to fill when aligning names |
19 | max_name_len: usize, |
20 | |
21 | is_multithreaded: bool, |
22 | } |
23 | |
24 | impl<T: Write> PrettyFormatter<T> { |
25 | pub fn new( |
26 | out: OutputLocation<T>, |
27 | use_color: bool, |
28 | max_name_len: usize, |
29 | is_multithreaded: bool, |
30 | time_options: Option<time::TestTimeOptions>, |
31 | ) -> Self { |
32 | PrettyFormatter { out, use_color, max_name_len, is_multithreaded, time_options } |
33 | } |
34 | |
35 | #[cfg (test)] |
36 | pub fn output_location(&self) -> &OutputLocation<T> { |
37 | &self.out |
38 | } |
39 | |
40 | pub fn write_ok(&mut self) -> io::Result<()> { |
41 | self.write_short_result("ok" , term::color::GREEN) |
42 | } |
43 | |
44 | pub fn write_failed(&mut self) -> io::Result<()> { |
45 | self.write_short_result("FAILED" , term::color::RED) |
46 | } |
47 | |
48 | pub fn write_ignored(&mut self, message: Option<&'static str>) -> io::Result<()> { |
49 | if let Some(message) = message { |
50 | self.write_short_result(&format!("ignored, {message}" ), term::color::YELLOW) |
51 | } else { |
52 | self.write_short_result("ignored" , term::color::YELLOW) |
53 | } |
54 | } |
55 | |
56 | pub fn write_time_failed(&mut self) -> io::Result<()> { |
57 | self.write_short_result("FAILED (time limit exceeded)" , term::color::RED) |
58 | } |
59 | |
60 | pub fn write_bench(&mut self) -> io::Result<()> { |
61 | self.write_pretty("bench" , term::color::CYAN) |
62 | } |
63 | |
64 | pub fn write_short_result( |
65 | &mut self, |
66 | result: &str, |
67 | color: term::color::Color, |
68 | ) -> io::Result<()> { |
69 | self.write_pretty(result, color) |
70 | } |
71 | |
72 | pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { |
73 | match self.out { |
74 | OutputLocation::Pretty(ref mut term) => { |
75 | if self.use_color { |
76 | term.fg(color)?; |
77 | } |
78 | term.write_all(word.as_bytes())?; |
79 | if self.use_color { |
80 | term.reset()?; |
81 | } |
82 | term.flush() |
83 | } |
84 | OutputLocation::Raw(ref mut stdout) => { |
85 | stdout.write_all(word.as_bytes())?; |
86 | stdout.flush() |
87 | } |
88 | } |
89 | } |
90 | |
91 | pub fn write_plain<S: AsRef<str>>(&mut self, s: S) -> io::Result<()> { |
92 | let s = s.as_ref(); |
93 | self.out.write_all(s.as_bytes())?; |
94 | self.out.flush() |
95 | } |
96 | |
97 | fn write_time( |
98 | &mut self, |
99 | desc: &TestDesc, |
100 | exec_time: Option<&time::TestExecTime>, |
101 | ) -> io::Result<()> { |
102 | if let (Some(opts), Some(time)) = (self.time_options, exec_time) { |
103 | let time_str = format!(" <{time}>" ); |
104 | |
105 | let color = if self.use_color { |
106 | if opts.is_critical(desc, time) { |
107 | Some(term::color::RED) |
108 | } else if opts.is_warn(desc, time) { |
109 | Some(term::color::YELLOW) |
110 | } else { |
111 | None |
112 | } |
113 | } else { |
114 | None |
115 | }; |
116 | |
117 | match color { |
118 | Some(color) => self.write_pretty(&time_str, color)?, |
119 | None => self.write_plain(&time_str)?, |
120 | } |
121 | } |
122 | |
123 | Ok(()) |
124 | } |
125 | |
126 | fn write_results( |
127 | &mut self, |
128 | inputs: &Vec<(TestDesc, Vec<u8>)>, |
129 | results_type: &str, |
130 | ) -> io::Result<()> { |
131 | let results_out_str = format!(" \n{results_type}: \n" ); |
132 | |
133 | self.write_plain(&results_out_str)?; |
134 | |
135 | let mut results = Vec::new(); |
136 | let mut stdouts = String::new(); |
137 | for (f, stdout) in inputs { |
138 | results.push(f.name.to_string()); |
139 | if !stdout.is_empty() { |
140 | stdouts.push_str(&format!("---- {} stdout ---- \n" , f.name)); |
141 | let output = String::from_utf8_lossy(stdout); |
142 | stdouts.push_str(&output); |
143 | stdouts.push(' \n' ); |
144 | } |
145 | } |
146 | if !stdouts.is_empty() { |
147 | self.write_plain(" \n" )?; |
148 | self.write_plain(&stdouts)?; |
149 | } |
150 | |
151 | self.write_plain(&results_out_str)?; |
152 | results.sort(); |
153 | for name in &results { |
154 | self.write_plain(&format!(" {name} \n" ))?; |
155 | } |
156 | Ok(()) |
157 | } |
158 | |
159 | pub fn write_successes(&mut self, state: &ConsoleTestState) -> io::Result<()> { |
160 | self.write_results(&state.not_failures, "successes" ) |
161 | } |
162 | |
163 | pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { |
164 | self.write_results(&state.failures, "failures" ) |
165 | } |
166 | |
167 | pub fn write_time_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { |
168 | self.write_results(&state.time_failures, "failures (time limit exceeded)" ) |
169 | } |
170 | |
171 | fn write_test_name(&mut self, desc: &TestDesc) -> io::Result<()> { |
172 | let name = desc.padded_name(self.max_name_len, desc.name.padding()); |
173 | if let Some(test_mode) = desc.test_mode() { |
174 | self.write_plain(format!("test {name} - {test_mode} ... " ))?; |
175 | } else { |
176 | self.write_plain(format!("test {name} ... " ))?; |
177 | } |
178 | |
179 | Ok(()) |
180 | } |
181 | } |
182 | |
183 | impl<T: Write> OutputFormatter for PrettyFormatter<T> { |
184 | fn write_discovery_start(&mut self) -> io::Result<()> { |
185 | Ok(()) |
186 | } |
187 | |
188 | fn write_test_discovered(&mut self, desc: &TestDesc, test_type: &str) -> io::Result<()> { |
189 | self.write_plain(format!("{}: {test_type} \n" , desc.name)) |
190 | } |
191 | |
192 | fn write_discovery_finish(&mut self, state: &ConsoleTestDiscoveryState) -> io::Result<()> { |
193 | fn plural(count: usize, s: &str) -> String { |
194 | match count { |
195 | 1 => format!("1 {s}" ), |
196 | n => format!("{n} {s}s" ), |
197 | } |
198 | } |
199 | |
200 | if state.tests != 0 || state.benchmarks != 0 { |
201 | self.write_plain(" \n" )?; |
202 | } |
203 | |
204 | self.write_plain(format!( |
205 | "{}, {} \n" , |
206 | plural(state.tests, "test" ), |
207 | plural(state.benchmarks, "benchmark" ) |
208 | )) |
209 | } |
210 | |
211 | fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> { |
212 | let noun = if test_count != 1 { "tests" } else { "test" }; |
213 | let shuffle_seed_msg = if let Some(shuffle_seed) = shuffle_seed { |
214 | format!(" (shuffle seed: {shuffle_seed})" ) |
215 | } else { |
216 | String::new() |
217 | }; |
218 | self.write_plain(format!(" \nrunning {test_count} {noun}{shuffle_seed_msg} \n" )) |
219 | } |
220 | |
221 | fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { |
222 | // When running tests concurrently, we should not print |
223 | // the test's name as the result will be mis-aligned. |
224 | // When running the tests serially, we print the name here so |
225 | // that the user can see which test hangs. |
226 | if !self.is_multithreaded { |
227 | self.write_test_name(desc)?; |
228 | } |
229 | |
230 | Ok(()) |
231 | } |
232 | |
233 | fn write_result( |
234 | &mut self, |
235 | desc: &TestDesc, |
236 | result: &TestResult, |
237 | exec_time: Option<&time::TestExecTime>, |
238 | _: &[u8], |
239 | _: &ConsoleTestState, |
240 | ) -> io::Result<()> { |
241 | if self.is_multithreaded { |
242 | self.write_test_name(desc)?; |
243 | } |
244 | |
245 | match *result { |
246 | TestResult::TrOk => self.write_ok()?, |
247 | TestResult::TrFailed | TestResult::TrFailedMsg(_) => self.write_failed()?, |
248 | TestResult::TrIgnored => self.write_ignored(desc.ignore_message)?, |
249 | TestResult::TrBench(ref bs) => { |
250 | self.write_bench()?; |
251 | self.write_plain(format!(": {}" , fmt_bench_samples(bs)))?; |
252 | } |
253 | TestResult::TrTimedFail => self.write_time_failed()?, |
254 | } |
255 | |
256 | self.write_time(desc, exec_time)?; |
257 | self.write_plain(" \n" ) |
258 | } |
259 | |
260 | fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { |
261 | self.write_plain(format!( |
262 | "test {} has been running for over {} seconds \n" , |
263 | desc.name, |
264 | time::TEST_WARN_TIMEOUT_S |
265 | )) |
266 | } |
267 | |
268 | fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> { |
269 | if state.options.display_output { |
270 | self.write_successes(state)?; |
271 | } |
272 | let success = state.failed == 0; |
273 | if !success { |
274 | if !state.failures.is_empty() { |
275 | self.write_failures(state)?; |
276 | } |
277 | |
278 | if !state.time_failures.is_empty() { |
279 | self.write_time_failures(state)?; |
280 | } |
281 | } |
282 | |
283 | self.write_plain(" \ntest result: " )?; |
284 | |
285 | if success { |
286 | // There's no parallelism at this point so it's safe to use color |
287 | self.write_pretty("ok" , term::color::GREEN)?; |
288 | } else { |
289 | self.write_pretty("FAILED" , term::color::RED)?; |
290 | } |
291 | |
292 | let s = format!( |
293 | ". {} passed; {} failed; {} ignored; {} measured; {} filtered out" , |
294 | state.passed, state.failed, state.ignored, state.measured, state.filtered_out |
295 | ); |
296 | |
297 | self.write_plain(s)?; |
298 | |
299 | if let Some(ref exec_time) = state.exec_time { |
300 | let time_str = format!("; finished in {exec_time}" ); |
301 | self.write_plain(time_str)?; |
302 | } |
303 | |
304 | self.write_plain(" \n\n" )?; |
305 | |
306 | Ok(success) |
307 | } |
308 | } |
309 | |