• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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> {
new(out: OutputLocation<T>) -> Self18     pub fn new(out: OutputLocation<T>) -> Self {
19         Self { out, results: Vec::new() }
20     }
21 
write_message(&mut self, s: &str) -> io::Result<()>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 
str_to_cdata(s: &str) -> String29 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 &#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 
41 impl<T: Write> OutputFormatter for JunitFormatter<T> {
write_discovery_start(&mut self) -> io::Result<()>42     fn write_discovery_start(&mut self) -> io::Result<()> {
43         Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
44     }
45 
write_test_discovered(&mut self, _desc: &TestDesc, _test_type: &str) -> io::Result<()>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 
write_discovery_finish(&mut self, _state: &ConsoleTestDiscoveryState) -> io::Result<()>50     fn write_discovery_finish(&mut self, _state: &ConsoleTestDiscoveryState) -> io::Result<()> {
51         Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
52     }
53 
write_run_start( &mut self, _test_count: usize, _shuffle_seed: Option<u64>, ) -> io::Result<()>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 
write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()>63     fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
64         // We do not output anything on test start.
65         Ok(())
66     }
67 
write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()>68     fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
69         // We do not output anything on test timeout.
70         Ok(())
71     }
72 
write_result( &mut self, desc: &TestDesc, result: &TestResult, exec_time: Option<&time::TestExecTime>, stdout: &[u8], _state: &ConsoleTestState, ) -> io::Result<()>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     }
write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool>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 
parse_class_name(desc: &TestDesc) -> (String, String)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 
parse_class_name_unit(desc: &TestDesc) -> (String, String)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 @ .., test] => (path.join("::"), String::from(test)),
204         [..] => unreachable!(),
205     };
206     (class_name, test_name)
207 }
208 
parse_class_name_doc(desc: &TestDesc) -> (String, String)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 
parse_class_name_integration(desc: &TestDesc) -> (String, String)220 fn parse_class_name_integration(desc: &TestDesc) -> (String, String) {
221     (String::from("integration"), String::from(desc.name.as_slice()))
222 }
223