1 use std::io::{self, prelude::Write};
2 use std::time::Duration;
4 use super::OutputFormatter;
6 console::{ConsoleTestState, OutputLocation},
7 test_result::TestResult,
9 types::{TestDesc, TestType},
12 pub struct JunitFormatter<T> {
13 out: OutputLocation<T>,
14 results: Vec<(TestDesc, TestResult, Duration)>,
17 impl<T: Write> JunitFormatter<T> {
18 pub fn new(out: OutputLocation<T>) -> Self {
19 Self { out, results: Vec::new() }
22 fn write_message(&mut self, s: &str) -> io::Result<()> {
23 assert!(!s.contains('\n'));
25 self.out.write_all(s.as_ref())
29 impl<T: Write> OutputFormatter for JunitFormatter<T> {
33 _shuffle_seed: Option<u64>,
35 // We write xml header on run start
36 self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
39 fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
40 // We do not output anything on test start.
44 fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
45 // We do not output anything on test timeout.
53 exec_time: Option<&time::TestExecTime>,
55 _state: &ConsoleTestState,
57 // Because the testsuite node holds some of the information as attributes, we can't write it
58 // until all of the tests have finished. Instead of writing every result as they come in, we add
59 // them to a Vec and write them all at once when run is complete.
60 let duration = exec_time.map(|t| t.0).unwrap_or_default();
61 self.results.push((desc.clone(), result.clone(), duration));
64 fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
65 self.write_message("<testsuites>")?;
67 self.write_message(&*format!(
68 "<testsuite name=\"test\" package=\"test\" id=\"0\" \
74 state.failed, state.total, state.ignored
76 for (desc, result, duration) in std::mem::replace(&mut self.results, Vec::new()) {
77 let (class_name, test_name) = parse_class_name(&desc);
79 TestResult::TrIgnored => { /* no-op */ }
80 TestResult::TrFailed => {
81 self.write_message(&*format!(
82 "<testcase classname=\"{}\" \
83 name=\"{}\" time=\"{}\">",
86 duration.as_secs_f64()
88 self.write_message("<failure type=\"assert\"/>")?;
89 self.write_message("</testcase>")?;
92 TestResult::TrFailedMsg(ref m) => {
93 self.write_message(&*format!(
94 "<testcase classname=\"{}\" \
95 name=\"{}\" time=\"{}\">",
98 duration.as_secs_f64()
100 self.write_message(&*format!("<failure message=\"{}\" type=\"assert\"/>", m))?;
101 self.write_message("</testcase>")?;
104 TestResult::TrTimedFail => {
105 self.write_message(&*format!(
106 "<testcase classname=\"{}\" \
107 name=\"{}\" time=\"{}\">",
110 duration.as_secs_f64()
112 self.write_message("<failure type=\"timeout\"/>")?;
113 self.write_message("</testcase>")?;
116 TestResult::TrBench(ref b) => {
117 self.write_message(&*format!(
118 "<testcase classname=\"benchmark::{}\" \
119 name=\"{}\" time=\"{}\" />",
120 class_name, test_name, b.ns_iter_summ.sum
124 TestResult::TrOk => {
125 self.write_message(&*format!(
126 "<testcase classname=\"{}\" \
127 name=\"{}\" time=\"{}\"/>",
130 duration.as_secs_f64()
135 self.write_message("<system-out/>")?;
136 self.write_message("<system-err/>")?;
137 self.write_message("</testsuite>")?;
138 self.write_message("</testsuites>")?;
140 self.out.write_all(b"\n")?;
142 Ok(state.failed == 0)
146 fn parse_class_name(desc: &TestDesc) -> (String, String) {
147 match desc.test_type {
148 TestType::UnitTest => parse_class_name_unit(desc),
149 TestType::DocTest => parse_class_name_doc(desc),
150 TestType::IntegrationTest => parse_class_name_integration(desc),
151 TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())),
155 fn parse_class_name_unit(desc: &TestDesc) -> (String, String) {
156 // Module path => classname
157 // Function name => name
158 let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect();
159 let (class_name, test_name) = match module_segments[..] {
160 [test] => (String::from("crate"), String::from(test)),
161 [ref path @ .., test] => (path.join("::"), String::from(test)),
162 [..] => unreachable!(),
164 (class_name, test_name)
167 fn parse_class_name_doc(desc: &TestDesc) -> (String, String) {
168 // File path => classname
169 // Line # => test name
170 let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect();
171 let (class_name, test_name) = match segments[..] {
172 [file, line] => (String::from(file.trim()), String::from(line.trim())),
173 [..] => unreachable!(),
175 (class_name, test_name)
178 fn parse_class_name_integration(desc: &TestDesc) -> (String, String) {
179 (String::from("integration"), String::from(desc.name.as_slice()))