summaryrefslogtreecommitdiffstats
path: root/library/test/src/formatters/junit.rs
diff options
context:
space:
mode:
Diffstat (limited to 'library/test/src/formatters/junit.rs')
-rw-r--r--library/test/src/formatters/junit.rs180
1 files changed, 180 insertions, 0 deletions
diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs
new file mode 100644
index 000000000..e6fb4f570
--- /dev/null
+++ b/library/test/src/formatters/junit.rs
@@ -0,0 +1,180 @@
+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<T> {
+ out: OutputLocation<T>,
+ results: Vec<(TestDesc, TestResult, Duration)>,
+}
+
+impl<T: Write> JunitFormatter<T> {
+ pub fn new(out: OutputLocation<T>) -> 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<T: Write> OutputFormatter for JunitFormatter<T> {
+ fn write_run_start(
+ &mut self,
+ _test_count: usize,
+ _shuffle_seed: Option<u64>,
+ ) -> io::Result<()> {
+ // We write xml header on run start
+ self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
+ }
+
+ 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<bool> {
+ self.write_message("<testsuites>")?;
+
+ self.write_message(&*format!(
+ "<testsuite name=\"test\" package=\"test\" id=\"0\" \
+ errors=\"0\" \
+ failures=\"{}\" \
+ tests=\"{}\" \
+ skipped=\"{}\" \
+ >",
+ 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!(
+ "<testcase classname=\"{}\" \
+ name=\"{}\" time=\"{}\">",
+ class_name,
+ test_name,
+ duration.as_secs_f64()
+ ))?;
+ self.write_message("<failure type=\"assert\"/>")?;
+ self.write_message("</testcase>")?;
+ }
+
+ TestResult::TrFailedMsg(ref m) => {
+ self.write_message(&*format!(
+ "<testcase classname=\"{}\" \
+ name=\"{}\" time=\"{}\">",
+ class_name,
+ test_name,
+ duration.as_secs_f64()
+ ))?;
+ self.write_message(&*format!("<failure message=\"{m}\" type=\"assert\"/>"))?;
+ self.write_message("</testcase>")?;
+ }
+
+ TestResult::TrTimedFail => {
+ self.write_message(&*format!(
+ "<testcase classname=\"{}\" \
+ name=\"{}\" time=\"{}\">",
+ class_name,
+ test_name,
+ duration.as_secs_f64()
+ ))?;
+ self.write_message("<failure type=\"timeout\"/>")?;
+ self.write_message("</testcase>")?;
+ }
+
+ TestResult::TrBench(ref b) => {
+ self.write_message(&*format!(
+ "<testcase classname=\"benchmark::{}\" \
+ name=\"{}\" time=\"{}\" />",
+ class_name, test_name, b.ns_iter_summ.sum
+ ))?;
+ }
+
+ TestResult::TrOk => {
+ self.write_message(&*format!(
+ "<testcase classname=\"{}\" \
+ name=\"{}\" time=\"{}\"/>",
+ class_name,
+ test_name,
+ duration.as_secs_f64()
+ ))?;
+ }
+ }
+ }
+ self.write_message("<system-out/>")?;
+ self.write_message("<system-err/>")?;
+ self.write_message("</testsuite>")?;
+ self.write_message("</testsuites>")?;
+
+ 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()))
+}