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 let escaped_output = s.replace("]]>", "]]]]><![CDATA[>");
32 let escaped_output = escaped_output.replace("<?", "<]]><![CDATA[?");
33 let escaped_output = escaped_output.replace('\n', "]]>
<![CDATA[");
35 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 self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
60 }
61
62 fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
63 Ok(())
65 }
66
67 fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
68 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 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 => { }
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 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 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}