1use std::io::{self, prelude::Write};
2use std::time::Duration;
3
4use super::OutputFormatter;
5use crate::{
6 console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation},
7 test_result::TestResult,
8 time,
9 types::{TestDesc, TestType},
10};
11
12pub struct JunitFormatter<T> {
13 out: OutputLocation<T>,
14 results: Vec<(TestDesc, TestResult, Duration, Vec<u8>)>,
15}
16
17impl<T: Write> JunitFormatter<T> {
18 pub fn new(out: OutputLocation<T>) -> Self {
19 Self { out, results: Vec::new() }
20 }
21
22 fn write_message(&mut self, s: &str) -> io::Result<()> {
23 assert!(!s.contains('\n'));
24
25 self.out.write_all(s.as_ref())
26 }
27}
28
29fn str_to_cdata(s: &str) -> String {
30 // Drop the stdout in a cdata. Unfortunately, you can't put either of `]]>` or
31 // `<?'` in a CDATA block, so the escaping gets a little weird.
32 let escaped_output = s.replace("]]>", "]]]]><![CDATA[>");
33 let escaped_output = escaped_output.replace("<?", "<]]><![CDATA[?");
34 // We also smuggle newlines as &#xa so as to keep all the output on one line
35 let escaped_output = escaped_output.replace('\n', "]]>&#xA;<![CDATA[");
36 // Prune empty CDATA blocks resulting from any escaping
37 let escaped_output = escaped_output.replace("<![CDATA[]]>", "");
38 format!("<![CDATA[{}]]>", escaped_output)
39}
40
41impl<T: Write> OutputFormatter for JunitFormatter<T> {
42 fn write_discovery_start(&mut self) -> io::Result<()> {
43 Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
44 }
45
46 fn write_test_discovered(&mut self, _desc: &TestDesc, _test_type: &str) -> io::Result<()> {
47 Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
48 }
49
50 fn write_discovery_finish(&mut self, _state: &ConsoleTestDiscoveryState) -> io::Result<()> {
51 Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
52 }
53
54 fn write_run_start(
55 &mut self,
56 _test_count: usize,
57 _shuffle_seed: Option<u64>,
58 ) -> io::Result<()> {
59 // We write xml header on run start
60 self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
61 }
62
63 fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
64 // We do not output anything on test start.
65 Ok(())
66 }
67
68 fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
69 // We do not output anything on test timeout.
70 Ok(())
71 }
72
73 fn write_result(
74 &mut self,
75 desc: &TestDesc,
76 result: &TestResult,
77 exec_time: Option<&time::TestExecTime>,
78 stdout: &[u8],
79 _state: &ConsoleTestState,
80 ) -> io::Result<()> {
81 // Because the testsuite node holds some of the information as attributes, we can't write it
82 // until all of the tests have finished. Instead of writing every result as they come in, we add
83 // them to a Vec and write them all at once when run is complete.
84 let duration = exec_time.map(|t| t.0).unwrap_or_default();
85 self.results.push((desc.clone(), result.clone(), duration, stdout.to_vec()));
86 Ok(())
87 }
88 fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
89 self.write_message("<testsuites>")?;
90
91 self.write_message(&format!(
92 "<testsuite name=\"test\" package=\"test\" id=\"0\" \
93 errors=\"0\" \
94 failures=\"{}\" \
95 tests=\"{}\" \
96 skipped=\"{}\" \
97 >",
98 state.failed, state.total, state.ignored
99 ))?;
100 for (desc, result, duration, stdout) in std::mem::take(&mut self.results) {
101 let (class_name, test_name) = parse_class_name(&desc);
102 match result {
103 TestResult::TrIgnored => { /* no-op */ }
104 TestResult::TrFailed => {
105 self.write_message(&format!(
106 "<testcase classname=\"{}\" \
107 name=\"{}\" time=\"{}\">",
108 class_name,
109 test_name,
110 duration.as_secs_f64()
111 ))?;
112 self.write_message("<failure type=\"assert\"/>")?;
113 if !stdout.is_empty() {
114 self.write_message("<system-out>")?;
115 self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
116 self.write_message("</system-out>")?;
117 }
118 self.write_message("</testcase>")?;
119 }
120
121 TestResult::TrFailedMsg(ref m) => {
122 self.write_message(&format!(
123 "<testcase classname=\"{}\" \
124 name=\"{}\" time=\"{}\">",
125 class_name,
126 test_name,
127 duration.as_secs_f64()
128 ))?;
129 self.write_message(&format!("<failure message=\"{m}\" type=\"assert\"/>"))?;
130 if !stdout.is_empty() {
131 self.write_message("<system-out>")?;
132 self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
133 self.write_message("</system-out>")?;
134 }
135 self.write_message("</testcase>")?;
136 }
137
138 TestResult::TrTimedFail => {
139 self.write_message(&format!(
140 "<testcase classname=\"{}\" \
141 name=\"{}\" time=\"{}\">",
142 class_name,
143 test_name,
144 duration.as_secs_f64()
145 ))?;
146 self.write_message("<failure type=\"timeout\"/>")?;
147 self.write_message("</testcase>")?;
148 }
149
150 TestResult::TrBench(ref b) => {
151 self.write_message(&format!(
152 "<testcase classname=\"benchmark::{}\" \
153 name=\"{}\" time=\"{}\" />",
154 class_name, test_name, b.ns_iter_summ.sum
155 ))?;
156 }
157
158 TestResult::TrOk => {
159 self.write_message(&format!(
160 "<testcase classname=\"{}\" \
161 name=\"{}\" time=\"{}\"",
162 class_name,
163 test_name,
164 duration.as_secs_f64()
165 ))?;
166 if stdout.is_empty() || !state.options.display_output {
167 self.write_message("/>")?;
168 } else {
169 self.write_message("><system-out>")?;
170 self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
171 self.write_message("</system-out>")?;
172 self.write_message("</testcase>")?;
173 }
174 }
175 }
176 }
177 self.write_message("<system-out/>")?;
178 self.write_message("<system-err/>")?;
179 self.write_message("</testsuite>")?;
180 self.write_message("</testsuites>")?;
181
182 self.out.write_all(b"\n")?;
183
184 Ok(state.failed == 0)
185 }
186}
187
188fn parse_class_name(desc: &TestDesc) -> (String, String) {
189 match desc.test_type {
190 TestType::UnitTest => parse_class_name_unit(desc),
191 TestType::DocTest => parse_class_name_doc(desc),
192 TestType::IntegrationTest => parse_class_name_integration(desc),
193 TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())),
194 }
195}
196
197fn parse_class_name_unit(desc: &TestDesc) -> (String, String) {
198 // Module path => classname
199 // Function name => name
200 let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect();
201 let (class_name, test_name) = match module_segments[..] {
202 [test] => (String::from("crate"), String::from(test)),
203 [ref path: &[{unknown}] @ .., test] => (path.join("::"), String::from(test)),
204 [..] => unreachable!(),
205 };
206 (class_name, test_name)
207}
208
209fn parse_class_name_doc(desc: &TestDesc) -> (String, String) {
210 // File path => classname
211 // Line # => test name
212 let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect();
213 let (class_name, test_name) = match segments[..] {
214 [file, line] => (String::from(file.trim()), String::from(line.trim())),
215 [..] => unreachable!(),
216 };
217 (class_name, test_name)
218}
219
220fn parse_class_name_integration(desc: &TestDesc) -> (String, String) {
221 (String::from("integration"), String::from(desc.name.as_slice()))
222}
223