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.out.write_all(b"\n")?;
37 self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
40 fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
41 // We do not output anything on test start.
45 fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
46 // We do not output anything on test timeout.
54 exec_time: Option<&time::TestExecTime>,
56 _state: &ConsoleTestState,
58 // Because the testsuite node holds some of the information as attributes, we can't write it
59 // until all of the tests have finished. Instead of writing every result as they come in, we add
60 // them to a Vec and write them all at once when run is complete.
61 let duration = exec_time.map(|t| t.0).unwrap_or_default();
62 self.results.push((desc.clone(), result.clone(), duration));
65 fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
66 self.write_message("<testsuites>")?;
68 self.write_message(&*format!(
69 "<testsuite name=\"test\" package=\"test\" id=\"0\" \
75 state.failed, state.total, state.ignored
77 for (desc, result, duration) in std::mem::replace(&mut self.results, Vec::new()) {
78 let (class_name, test_name) = parse_class_name(&desc);
80 TestResult::TrIgnored => { /* no-op */ }
81 TestResult::TrFailed => {
82 self.write_message(&*format!(
83 "<testcase classname=\"{}\" \
84 name=\"{}\" time=\"{}\">",
87 duration.as_secs_f64()
89 self.write_message("<failure type=\"assert\"/>")?;
90 self.write_message("</testcase>")?;
93 TestResult::TrFailedMsg(ref m) => {
94 self.write_message(&*format!(
95 "<testcase classname=\"{}\" \
96 name=\"{}\" time=\"{}\">",
99 duration.as_secs_f64()
101 self.write_message(&*format!("<failure message=\"{}\" type=\"assert\"/>", m))?;
102 self.write_message("</testcase>")?;
105 TestResult::TrTimedFail => {
106 self.write_message(&*format!(
107 "<testcase classname=\"{}\" \
108 name=\"{}\" time=\"{}\">",
111 duration.as_secs_f64()
113 self.write_message("<failure type=\"timeout\"/>")?;
114 self.write_message("</testcase>")?;
117 TestResult::TrBench(ref b) => {
118 self.write_message(&*format!(
119 "<testcase classname=\"benchmark::{}\" \
120 name=\"{}\" time=\"{}\" />",
121 class_name, test_name, b.ns_iter_summ.sum
125 TestResult::TrOk => {
126 self.write_message(&*format!(
127 "<testcase classname=\"{}\" \
128 name=\"{}\" time=\"{}\"/>",
131 duration.as_secs_f64()
136 self.write_message("<system-out/>")?;
137 self.write_message("<system-err/>")?;
138 self.write_message("</testsuite>")?;
139 self.write_message("</testsuites>")?;
141 self.out.write_all(b"\n\n")?;
143 Ok(state.failed == 0)
147 fn parse_class_name(desc: &TestDesc) -> (String, String) {
148 match desc.test_type {
149 TestType::UnitTest => parse_class_name_unit(desc),
150 TestType::DocTest => parse_class_name_doc(desc),
151 TestType::IntegrationTest => parse_class_name_integration(desc),
152 TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())),
156 fn parse_class_name_unit(desc: &TestDesc) -> (String, String) {
157 // Module path => classname
158 // Function name => name
159 let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect();
160 let (class_name, test_name) = match module_segments[..] {
161 [test] => (String::from("crate"), String::from(test)),
162 [ref path @ .., test] => (path.join("::"), String::from(test)),
163 [..] => unreachable!(),
165 (class_name, test_name)
168 fn parse_class_name_doc(desc: &TestDesc) -> (String, String) {
169 // File path => classname
170 // Line # => test name
171 let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect();
172 let (class_name, test_name) = match segments[..] {
173 [file, line] => (String::from(file.trim()), String::from(line.trim())),
174 [..] => unreachable!(),
176 (class_name, test_name)
179 fn parse_class_name_integration(desc: &TestDesc) -> (String, String) {
180 (String::from("integration"), String::from(desc.name.as_slice()))