1 | use std::io::{self, prelude::Write}; |
2 | use std::time::Duration; |
3 | |
4 | use super::OutputFormatter; |
5 | use crate::{ |
6 | console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, |
7 | test_result::TestResult, |
8 | time, |
9 | types::{TestDesc, TestType}, |
10 | }; |
11 | |
12 | pub struct JunitFormatter<T> { |
13 | out: OutputLocation<T>, |
14 | results: Vec<(TestDesc, TestResult, Duration, Vec<u8>)>, |
15 | } |
16 | |
17 | impl<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 | |
29 | fn 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 
 so as to keep all the output on one line |
35 | let escaped_output = escaped_output.replace(' \n' , "]]>
<![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 | |
41 | impl<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 | |
188 | fn 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 | |
197 | fn 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 | |
209 | fn 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 | |
220 | fn parse_class_name_integration(desc: &TestDesc) -> (String, String) { |
221 | (String::from("integration" ), String::from(desc.name.as_slice())) |
222 | } |
223 | |