feat(libtest): Add JUnit formatter

This commit is contained in:
Andrey Cherkashin 2021-04-25 14:29:24 -07:00
parent 58bdb08947
commit 38485a9e34
5 changed files with 150 additions and 5 deletions

View File

@ -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
));

View File

@ -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)?;

View File

@ -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)
}
}

View File

@ -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;

View File

@ -39,6 +39,8 @@ pub enum OutputFormat {
Terse,
/// JSON output
Json,
/// JUnit output
Junit,
}
/// Whether ignored test should be run or not