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