Skip to content

feat(libtest): Add JUnit formatter #84568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 27, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 52 additions & 12 deletions library/test/src/formatters/junit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
console::{ConsoleTestState, OutputLocation},
test_result::TestResult,
time,
types::TestDesc,
types::{TestDesc, TestType},
};

pub struct JunitFormatter<T> {
Expand Down Expand Up @@ -70,13 +70,15 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
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=\"test.global\" \
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\">",
desc.name.as_slice(),
class_name,
test_name,
duration.as_secs()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if the output supports subsecond precision for the duration. This is what cargo2junit does. That way you don't loose the runtime information for fast tests.

))?;
self.write_message("<failure type=\"assert\"/>")?;
Expand All @@ -85,9 +87,10 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {

TestResult::TrFailedMsg(ref m) => {
self.write_message(&*format!(
"<testcase classname=\"test.global\" \
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\">",
desc.name.as_slice(),
class_name,
test_name,
duration.as_secs()
))?;
self.write_message(&*format!("<failure message=\"{}\" type=\"assert\"/>", m))?;
Expand All @@ -96,9 +99,10 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {

TestResult::TrTimedFail => {
self.write_message(&*format!(
"<testcase classname=\"test.global\" \
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\">",
desc.name.as_slice(),
class_name,
test_name,
duration.as_secs()
))?;
self.write_message("<failure type=\"timeout\"/>")?;
Expand All @@ -107,18 +111,18 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {

TestResult::TrBench(ref b) => {
self.write_message(&*format!(
"<testcase classname=\"benchmark.global\" \
"<testcase classname=\"benchmark::{}\" \
name=\"{}\" time=\"{}\" />",
desc.name.as_slice(),
b.ns_iter_summ.sum
class_name, test_name, b.ns_iter_summ.sum
))?;
}

TestResult::TrOk | TestResult::TrAllowedFail => {
self.write_message(&*format!(
"<testcase classname=\"test.global\" \
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\"/>",
desc.name.as_slice(),
class_name,
test_name,
duration.as_secs()
))?;
}
Expand All @@ -132,3 +136,39 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
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()))
}