about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAndrey Cherkashin <andoriyu@gmail.com>2021-04-25 14:29:24 -0700
committerAndrey Cherkashin <andoriyu@gmail.com>2021-04-25 15:51:50 -0700
commit38485a9e3445b1e926da293448fd056ca78cf156 (patch)
tree2d2901bacc3e21300d3be1dcc5445836b8ce8d4a
parent58bdb08947f5b3c18a2fbafc5cf36af7b5677d83 (diff)
downloadrust-38485a9e3445b1e926da293448fd056ca78cf156.tar.gz
rust-38485a9e3445b1e926da293448fd056ca78cf156.zip
feat(libtest): Add JUnit formatter
-rw-r--r--library/test/src/cli.rs14
-rw-r--r--library/test/src/console.rs3
-rw-r--r--library/test/src/formatters/junit.rs134
-rw-r--r--library/test/src/formatters/mod.rs2
-rw-r--r--library/test/src/options.rs2
5 files changed, 150 insertions, 5 deletions
diff --git a/library/test/src/cli.rs b/library/test/src/cli.rs
index b7791b1b24d..84874a2d225 100644
--- a/library/test/src/cli.rs
+++ b/library/test/src/cli.rs
@@ -95,8 +95,9 @@ fn optgroups() -> getopts::Options {
             "Configure formatting of output:
             pretty = Print verbose output;
             terse  = Display one character per test;
-            json   = Output a json document",
-            "pretty|terse|json",
+            json   = Output a json document;
+            junit  = Output a JUnit document",
+            "pretty|terse|json|junit",
         )
         .optflag("", "show-output", "Show captured stdout of successful tests")
         .optopt(
@@ -336,10 +337,15 @@ fn get_format(
             }
             OutputFormat::Json
         }
-
+        Some("junit") => {
+            if !allow_unstable {
+                return Err("The \"junit\" format is only accepted on the nightly compiler".into());
+            }
+            OutputFormat::Junit
+        }
         Some(v) => {
             return Err(format!(
-                "argument for --format must be pretty, terse, or json (was \
+                "argument for --format must be pretty, terse, json or junit (was \
                  {})",
                 v
             ));
diff --git a/library/test/src/console.rs b/library/test/src/console.rs
index 1721c3c14f9..9cfc7eaf4bc 100644
--- a/library/test/src/console.rs
+++ b/library/test/src/console.rs
@@ -10,7 +10,7 @@ use super::{
     cli::TestOpts,
     event::{CompletedTest, TestEvent},
     filter_tests,
-    formatters::{JsonFormatter, OutputFormatter, PrettyFormatter, TerseFormatter},
+    formatters::{JsonFormatter, JunitFormatter, OutputFormatter, PrettyFormatter, TerseFormatter},
     helpers::{concurrency::get_concurrency, metrics::MetricMap},
     options::{Options, OutputFormat},
     run_tests,
@@ -277,6 +277,7 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
             Box::new(TerseFormatter::new(output, opts.use_color(), max_name_len, is_multithreaded))
         }
         OutputFormat::Json => Box::new(JsonFormatter::new(output)),
+        OutputFormat::Junit => Box::new(JunitFormatter::new(output)),
     };
     let mut st = ConsoleTestState::new(opts)?;
 
diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs
new file mode 100644
index 00000000000..1d3c4ab604c
--- /dev/null
+++ b/library/test/src/formatters/junit.rs
@@ -0,0 +1,134 @@
+use std::io::{self, prelude::Write};
+use std::time::Duration;
+
+use super::OutputFormatter;
+use crate::{
+    console::{ConsoleTestState, OutputLocation},
+    test_result::TestResult,
+    time,
+    types::TestDesc,
+};
+
+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) -> 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 testsuit node holds some of the information as attributes, we can't write it
+        // until all of the tests has ran. Instead of writting 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.clone()).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()) {
+            match result {
+                TestResult::TrIgnored => { /* no-op */ }
+                TestResult::TrFailed => {
+                    self.write_message(&*format!(
+                        "<testcase classname=\"test.global\" \
+                         name=\"{}\" time=\"{}\">",
+                        desc.name.as_slice(),
+                        duration.as_secs()
+                    ))?;
+                    self.write_message("<failure type=\"assert\"/>")?;
+                    self.write_message("</testcase>")?;
+                }
+
+                TestResult::TrFailedMsg(ref m) => {
+                    self.write_message(&*format!(
+                        "<testcase classname=\"test.global\" \
+                         name=\"{}\" time=\"{}\">",
+                        desc.name.as_slice(),
+                        duration.as_secs()
+                    ))?;
+                    self.write_message(&*format!("<failure message=\"{}\" type=\"assert\"/>", m))?;
+                    self.write_message("</testcase>")?;
+                }
+
+                TestResult::TrTimedFail => {
+                    self.write_message(&*format!(
+                        "<testcase classname=\"test.global\" \
+                         name=\"{}\" time=\"{}\">",
+                        desc.name.as_slice(),
+                        duration.as_secs()
+                    ))?;
+                    self.write_message("<failure type=\"timeout\"/>")?;
+                    self.write_message("</testcase>")?;
+                }
+
+                TestResult::TrBench(ref b) => {
+                    self.write_message(&*format!(
+                        "<testcase classname=\"benchmark.global\" \
+                         name=\"{}\" time=\"{}\" />",
+                        desc.name.as_slice(),
+                        b.ns_iter_summ.sum
+                    ))?;
+                }
+
+                TestResult::TrOk | TestResult::TrAllowedFail => {
+                    self.write_message(&*format!(
+                        "<testcase classname=\"test.global\" \
+                         name=\"{}\" time=\"{}\"/>",
+                        desc.name.as_slice(),
+                        duration.as_secs()
+                    ))?;
+                }
+            }
+        }
+        self.write_message("<system-out/>")?;
+        self.write_message("<system-err/>")?;
+        self.write_message("</testsuite>")?;
+        self.write_message("</testsuites>")?;
+
+        Ok(state.failed == 0)
+    }
+}
diff --git a/library/test/src/formatters/mod.rs b/library/test/src/formatters/mod.rs
index 1fb840520a6..2e03581b3af 100644
--- a/library/test/src/formatters/mod.rs
+++ b/library/test/src/formatters/mod.rs
@@ -8,10 +8,12 @@ use crate::{
 };
 
 mod json;
+mod junit;
 mod pretty;
 mod terse;
 
 pub(crate) use self::json::JsonFormatter;
+pub(crate) use self::junit::JunitFormatter;
 pub(crate) use self::pretty::PrettyFormatter;
 pub(crate) use self::terse::TerseFormatter;
 
diff --git a/library/test/src/options.rs b/library/test/src/options.rs
index 8e7bd8de924..baf36b5f1d8 100644
--- a/library/test/src/options.rs
+++ b/library/test/src/options.rs
@@ -39,6 +39,8 @@ pub enum OutputFormat {
     Terse,
     /// JSON output
     Json,
+    /// JUnit output
+    Junit,
 }
 
 /// Whether ignored test should be run or not