use std::io::{self, prelude::Write}; use std::time::Duration; use super::OutputFormatter; use crate::{ console::{ConsoleTestState, OutputLocation}, test_result::TestResult, time, types::{TestDesc, TestType}, }; pub struct JunitFormatter { out: OutputLocation, results: Vec<(TestDesc, TestResult, Duration)>, } impl JunitFormatter { pub fn new(out: OutputLocation) -> Self { Self { out, results: Vec::new() } } fn write_message(&mut self, s: &str) -> io::Result<()> { assert!(!s.contains('\n')); self.out.write_all(s.as_ref()) } } impl OutputFormatter for JunitFormatter { fn write_run_start( &mut self, _test_count: usize, _shuffle_seed: Option, ) -> io::Result<()> { // We write xml header on run start self.write_message("") } fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> { // We do not output anything on test start. Ok(()) } fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> { // We do not output anything on test timeout. Ok(()) } fn write_result( &mut self, desc: &TestDesc, result: &TestResult, exec_time: Option<&time::TestExecTime>, _stdout: &[u8], _state: &ConsoleTestState, ) -> io::Result<()> { // Because the testsuite node holds some of the information as attributes, we can't write it // until all of the tests have finished. Instead of writing every result as they come in, we add // them to a Vec and write them all at once when run is complete. let duration = exec_time.map(|t| t.0).unwrap_or_default(); self.results.push((desc.clone(), result.clone(), duration)); Ok(()) } fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { self.write_message("")?; self.write_message(&*format!( "", state.failed, state.total, state.ignored ))?; for (desc, result, duration) in std::mem::replace(&mut self.results, Vec::new()) { let (class_name, test_name) = parse_class_name(&desc); match result { TestResult::TrIgnored => { /* no-op */ } TestResult::TrFailed => { self.write_message(&*format!( "", class_name, test_name, duration.as_secs_f64() ))?; self.write_message("")?; self.write_message("")?; } TestResult::TrFailedMsg(ref m) => { self.write_message(&*format!( "", class_name, test_name, duration.as_secs_f64() ))?; self.write_message(&*format!(""))?; self.write_message("")?; } TestResult::TrTimedFail => { self.write_message(&*format!( "", class_name, test_name, duration.as_secs_f64() ))?; self.write_message("")?; self.write_message("")?; } TestResult::TrBench(ref b) => { self.write_message(&*format!( "", class_name, test_name, b.ns_iter_summ.sum ))?; } TestResult::TrOk => { self.write_message(&*format!( "", class_name, test_name, duration.as_secs_f64() ))?; } } } self.write_message("")?; self.write_message("")?; self.write_message("")?; self.write_message("")?; self.out.write_all(b"\n")?; Ok(state.failed == 0) } } fn parse_class_name(desc: &TestDesc) -> (String, String) { match desc.test_type { TestType::UnitTest => parse_class_name_unit(desc), TestType::DocTest => parse_class_name_doc(desc), TestType::IntegrationTest => parse_class_name_integration(desc), TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())), } } fn parse_class_name_unit(desc: &TestDesc) -> (String, String) { // Module path => classname // Function name => name let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect(); let (class_name, test_name) = match module_segments[..] { [test] => (String::from("crate"), String::from(test)), [ref path @ .., test] => (path.join("::"), String::from(test)), [..] => unreachable!(), }; (class_name, test_name) } fn parse_class_name_doc(desc: &TestDesc) -> (String, String) { // File path => classname // Line # => test name let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect(); let (class_name, test_name) = match segments[..] { [file, line] => (String::from(file.trim()), String::from(line.trim())), [..] => unreachable!(), }; (class_name, test_name) } fn parse_class_name_integration(desc: &TestDesc) -> (String, String) { (String::from("integration"), String::from(desc.name.as_slice())) }