test/formatters/
junit.rs

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