1 | use std::{borrow::Cow, io, io::prelude::Write}; |
2 | |
3 | use super::OutputFormatter; |
4 | use crate::{ |
5 | console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, |
6 | test_result::TestResult, |
7 | time, |
8 | types::TestDesc, |
9 | }; |
10 | |
11 | pub(crate) struct JsonFormatter<T> { |
12 | out: OutputLocation<T>, |
13 | } |
14 | |
15 | impl<T: Write> JsonFormatter<T> { |
16 | pub fn new(out: OutputLocation<T>) -> Self { |
17 | Self { out } |
18 | } |
19 | |
20 | fn writeln_message(&mut self, s: &str) -> io::Result<()> { |
21 | // self.out will take a lock, but that lock is released when write_all returns. This |
22 | // results in a race condition and json output may not end with a new line. We avoid this |
23 | // by issuing `write_all` calls line-by-line. |
24 | assert_eq!(s.chars().last(), Some(' \n' )); |
25 | |
26 | self.out.write_all(s.as_ref()) |
27 | } |
28 | |
29 | fn write_event( |
30 | &mut self, |
31 | ty: &str, |
32 | name: &str, |
33 | event: &str, |
34 | exec_time: Option<&time::TestExecTime>, |
35 | stdout: Option<Cow<'_, str>>, |
36 | extra: Option<&str>, |
37 | ) -> io::Result<()> { |
38 | // A doc test's name includes a filename which must be escaped for correct json. |
39 | let name = EscapedString(name); |
40 | let exec_time_json = if let Some(exec_time) = exec_time { |
41 | format!(r#", "exec_time": {}"# , exec_time.0.as_secs_f64()) |
42 | } else { |
43 | String::from("" ) |
44 | }; |
45 | let stdout_json = if let Some(stdout) = stdout { |
46 | format!(r#", "stdout": "{}""# , EscapedString(stdout)) |
47 | } else { |
48 | String::from("" ) |
49 | }; |
50 | let extra_json = |
51 | if let Some(extra) = extra { format!(r#", {extra}"# ) } else { String::from("" ) }; |
52 | let newline = " \n" ; |
53 | |
54 | self.writeln_message(&format!( |
55 | r#"{{ "type": "{ty}", "name": "{name}", "event": "{event}"{exec_time_json}{stdout_json}{extra_json} }}{newline}"# )) |
56 | } |
57 | } |
58 | |
59 | impl<T: Write> OutputFormatter for JsonFormatter<T> { |
60 | fn write_discovery_start(&mut self) -> io::Result<()> { |
61 | self.writeln_message(concat!(r#"{ "type": "suite", "event": "discovery" }"# , " \n" )) |
62 | } |
63 | |
64 | fn write_test_discovered(&mut self, desc: &TestDesc, test_type: &str) -> io::Result<()> { |
65 | let TestDesc { |
66 | name, |
67 | ignore, |
68 | ignore_message, |
69 | source_file, |
70 | start_line, |
71 | start_col, |
72 | end_line, |
73 | end_col, |
74 | .. |
75 | } = desc; |
76 | |
77 | let name = EscapedString(name.as_slice()); |
78 | let ignore_message = ignore_message.unwrap_or("" ); |
79 | let source_path = EscapedString(source_file); |
80 | let newline = " \n" ; |
81 | |
82 | self.writeln_message(&format!( |
83 | r#"{{ "type": "{test_type}", "event": "discovered", "name": "{name}", "ignore": {ignore}, "ignore_message": "{ignore_message}", "source_path": "{source_path}", "start_line": {start_line}, "start_col": {start_col}, "end_line": {end_line}, "end_col": {end_col} }}{newline}"# |
84 | )) |
85 | } |
86 | |
87 | fn write_discovery_finish(&mut self, state: &ConsoleTestDiscoveryState) -> io::Result<()> { |
88 | let ConsoleTestDiscoveryState { tests, benchmarks, ignored, .. } = state; |
89 | |
90 | let total = tests + benchmarks; |
91 | let newline = " \n" ; |
92 | self.writeln_message(&format!( |
93 | r#"{{ "type": "suite", "event": "completed", "tests": {tests}, "benchmarks": {benchmarks}, "total": {total}, "ignored": {ignored} }}{newline}"# |
94 | )) |
95 | } |
96 | |
97 | fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> { |
98 | let shuffle_seed_json = if let Some(shuffle_seed) = shuffle_seed { |
99 | format!(r#", "shuffle_seed": {shuffle_seed}"# ) |
100 | } else { |
101 | String::new() |
102 | }; |
103 | let newline = " \n" ; |
104 | self.writeln_message(&format!( |
105 | r#"{{ "type": "suite", "event": "started", "test_count": {test_count}{shuffle_seed_json} }}{newline}"# |
106 | )) |
107 | } |
108 | |
109 | fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { |
110 | let name = EscapedString(desc.name.as_slice()); |
111 | let newline = " \n" ; |
112 | self.writeln_message(&format!( |
113 | r#"{{ "type": "test", "event": "started", "name": "{name}" }}{newline}"# |
114 | )) |
115 | } |
116 | |
117 | fn write_result( |
118 | &mut self, |
119 | desc: &TestDesc, |
120 | result: &TestResult, |
121 | exec_time: Option<&time::TestExecTime>, |
122 | stdout: &[u8], |
123 | state: &ConsoleTestState, |
124 | ) -> io::Result<()> { |
125 | let display_stdout = state.options.display_output || *result != TestResult::TrOk; |
126 | let stdout = if display_stdout && !stdout.is_empty() { |
127 | Some(String::from_utf8_lossy(stdout)) |
128 | } else { |
129 | None |
130 | }; |
131 | match *result { |
132 | TestResult::TrOk => { |
133 | self.write_event("test" , desc.name.as_slice(), "ok" , exec_time, stdout, None) |
134 | } |
135 | |
136 | TestResult::TrFailed => { |
137 | self.write_event("test" , desc.name.as_slice(), "failed" , exec_time, stdout, None) |
138 | } |
139 | |
140 | TestResult::TrTimedFail => self.write_event( |
141 | "test" , |
142 | desc.name.as_slice(), |
143 | "failed" , |
144 | exec_time, |
145 | stdout, |
146 | Some(r#""reason": "time limit exceeded""# ), |
147 | ), |
148 | |
149 | TestResult::TrFailedMsg(ref m) => self.write_event( |
150 | "test" , |
151 | desc.name.as_slice(), |
152 | "failed" , |
153 | exec_time, |
154 | stdout, |
155 | Some(&*format!(r#""message": "{}""# , EscapedString(m))), |
156 | ), |
157 | |
158 | TestResult::TrIgnored => self.write_event( |
159 | "test" , |
160 | desc.name.as_slice(), |
161 | "ignored" , |
162 | exec_time, |
163 | stdout, |
164 | desc.ignore_message |
165 | .map(|msg| format!(r#""message": "{}""# , EscapedString(msg))) |
166 | .as_deref(), |
167 | ), |
168 | |
169 | TestResult::TrBench(ref bs) => { |
170 | let median = bs.ns_iter_summ.median as usize; |
171 | let deviation = (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as usize; |
172 | |
173 | let mbps = if bs.mb_s == 0 { |
174 | String::new() |
175 | } else { |
176 | format!(r#", "mib_per_second": {}"# , bs.mb_s) |
177 | }; |
178 | let name = EscapedString(desc.name.as_slice()); |
179 | |
180 | self.writeln_message(&format!( |
181 | "{{ \"type \": \"bench \", \ |
182 | \"name \": \"{name} \", \ |
183 | \"median \": {median}, \ |
184 | \"deviation \": {deviation}{mbps} }} \n" , |
185 | )) |
186 | } |
187 | } |
188 | } |
189 | |
190 | fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { |
191 | let name = EscapedString(desc.name.as_slice()); |
192 | let newline = " \n" ; |
193 | self.writeln_message(&format!( |
194 | r#"{{ "type": "test", "event": "timeout", "name": "{name}" }}{newline}"# , |
195 | )) |
196 | } |
197 | |
198 | fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> { |
199 | let event = if state.failed == 0 { "ok" } else { "failed" }; |
200 | let passed = state.passed; |
201 | let failed = state.failed; |
202 | let ignored = state.ignored; |
203 | let measured = state.measured; |
204 | let filtered_out = state.filtered_out; |
205 | let exec_time_json = if let Some(ref exec_time) = state.exec_time { |
206 | format!(r#", "exec_time": {}"# , exec_time.0.as_secs_f64()) |
207 | } else { |
208 | String::from("" ) |
209 | }; |
210 | let newline = " \n" ; |
211 | |
212 | self.writeln_message(&format!( |
213 | r#"{{ "type": "suite", "event": "{event}", "passed": {passed}, "failed": {failed}, "ignored": {ignored}, "measured": {measured}, "filtered_out": {filtered_out}{exec_time_json} }}{newline}"# |
214 | ))?; |
215 | |
216 | Ok(state.failed == 0) |
217 | } |
218 | } |
219 | |
220 | /// A formatting utility used to print strings with characters in need of escaping. |
221 | /// Base code taken form `libserialize::json::escape_str` |
222 | struct EscapedString<S: AsRef<str>>(S); |
223 | |
224 | impl<S: AsRef<str>> std::fmt::Display for EscapedString<S> { |
225 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result { |
226 | let mut start = 0; |
227 | |
228 | for (i, byte) in self.0.as_ref().bytes().enumerate() { |
229 | let escaped = match byte { |
230 | b'"' => " \\\"" , |
231 | b' \\' => " \\\\" , |
232 | b' \x00' => " \\u0000" , |
233 | b' \x01' => " \\u0001" , |
234 | b' \x02' => " \\u0002" , |
235 | b' \x03' => " \\u0003" , |
236 | b' \x04' => " \\u0004" , |
237 | b' \x05' => " \\u0005" , |
238 | b' \x06' => " \\u0006" , |
239 | b' \x07' => " \\u0007" , |
240 | b' \x08' => " \\b" , |
241 | b' \t' => " \\t" , |
242 | b' \n' => " \\n" , |
243 | b' \x0b' => " \\u000b" , |
244 | b' \x0c' => " \\f" , |
245 | b' \r' => " \\r" , |
246 | b' \x0e' => " \\u000e" , |
247 | b' \x0f' => " \\u000f" , |
248 | b' \x10' => " \\u0010" , |
249 | b' \x11' => " \\u0011" , |
250 | b' \x12' => " \\u0012" , |
251 | b' \x13' => " \\u0013" , |
252 | b' \x14' => " \\u0014" , |
253 | b' \x15' => " \\u0015" , |
254 | b' \x16' => " \\u0016" , |
255 | b' \x17' => " \\u0017" , |
256 | b' \x18' => " \\u0018" , |
257 | b' \x19' => " \\u0019" , |
258 | b' \x1a' => " \\u001a" , |
259 | b' \x1b' => " \\u001b" , |
260 | b' \x1c' => " \\u001c" , |
261 | b' \x1d' => " \\u001d" , |
262 | b' \x1e' => " \\u001e" , |
263 | b' \x1f' => " \\u001f" , |
264 | b' \x7f' => " \\u007f" , |
265 | _ => { |
266 | continue; |
267 | } |
268 | }; |
269 | |
270 | if start < i { |
271 | f.write_str(&self.0.as_ref()[start..i])?; |
272 | } |
273 | |
274 | f.write_str(escaped)?; |
275 | |
276 | start = i + 1; |
277 | } |
278 | |
279 | if start != self.0.as_ref().len() { |
280 | f.write_str(&self.0.as_ref()[start..])?; |
281 | } |
282 | |
283 | Ok(()) |
284 | } |
285 | } |
286 | |