1use std::{borrow::Cow, io, io::prelude::Write};
2
3use super::OutputFormatter;
4use crate::{
5 console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation},
6 test_result::TestResult,
7 time,
8 types::TestDesc,
9};
10
11pub(crate) struct JsonFormatter<T> {
12 out: OutputLocation<T>,
13}
14
15impl<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
59impl<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`
222struct EscapedString<S: AsRef<str>>(S);
223
224impl<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