Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions crates/libtest2-harness/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pre-release-replacements = [
[features]
default = []
json = ["dep:serde", "dep:serde_json"]
junit = []

[dependencies]
anstream = "0.3.1"
Expand Down
15 changes: 13 additions & 2 deletions crates/libtest2-harness/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,23 @@ fn notifier(opts: &libtest_lexarg::TestOpts) -> std::io::Result<Box<dyn notify::
OutputFormat::Json => Box::new(notify::JsonNotifier::new(stdout)),
#[cfg(not(feature = "json"))]
OutputFormat::Json => {
return Err(std::io::Error::new(std::io::ErrorKind::Other, ""));
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"`--format=json` is not supported",
));
}
_ if opts.list => Box::new(notify::TerseListNotifier::new(stdout)),
OutputFormat::Pretty => Box::new(notify::PrettyRunNotifier::new(stdout)),
OutputFormat::Terse => Box::new(notify::TerseRunNotifier::new(stdout)),
OutputFormat::Junit => todo!(),
#[cfg(feature = "junit")]
OutputFormat::Junit => Box::new(notify::JunitRunNotifier::new(stdout)),
#[cfg(not(feature = "junit"))]
OutputFormat::Junit => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"`--format=junit` is not supported",
));
}
};
Ok(notifier)
}
Expand Down
121 changes: 121 additions & 0 deletions crates/libtest2-harness/src/notify/junit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use super::Event;
use super::RunStatus;

#[derive(Debug)]
pub(crate) struct JunitRunNotifier<W> {
writer: W,
events: Vec<Event>,
}

impl<W: std::io::Write> JunitRunNotifier<W> {
pub(crate) fn new(writer: W) -> Self {
Self {
writer,
events: Vec::new(),
}
}
}

impl<W: std::io::Write> super::Notifier for JunitRunNotifier<W> {
fn notify(&mut self, event: Event) -> std::io::Result<()> {
let finished = matches!(&event, Event::SuiteComplete { .. });
self.events.push(event);
if finished {
let mut num_run = 0;
let mut num_failed = 0;
let mut num_ignored = 0;
for event in &self.events {
match event {
Event::DiscoverStart => {}
Event::DiscoverCase { run, .. } => {
if *run {
num_run += 1;
}
}
Event::DiscoverComplete { .. } => {}
Event::SuiteStart => {}
Event::CaseStart { .. } => {}
Event::CaseComplete { status, .. } => match status {
Some(RunStatus::Ignored) => {
num_ignored += 1;
}
Some(RunStatus::Failed) => {
num_failed += 1;
}
None => {}
},
Event::SuiteComplete { .. } => {}
}
}

writeln!(self.writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
writeln!(self.writer, "<testsuites>")?;

writeln!(
self.writer,
"<testsuite name=\"test\" package=\"test\" id=\"0\" \
tests=\"{num_run}\" \
errors=\"0\" \
failures=\"{num_failed}\" \
skipped=\"{num_ignored}\" \
>"
)?;
for event in std::mem::take(&mut self.events) {
if let Event::CaseComplete {
name,
status,
message,
elapsed_s,
..
} = event
{
let (class_name, test_name) = parse_class_name(&name);
let elapsed_s = elapsed_s.unwrap_or_default();
match status {
Some(RunStatus::Ignored) => {}
Some(RunStatus::Failed) => {
writeln!(
self.writer,
"<testcase classname=\"{class_name}\" \
name=\"{test_name}\" time=\"{elapsed_s}\">",
)?;
if let Some(message) = message {
writeln!(
self.writer,
"<failure message=\"{message}\" type=\"assert\"/>"
)?;
} else {
writeln!(self.writer, "<failure type=\"assert\"/>")?;
}
writeln!(self.writer, "</testcase>")?;
}
None => {
writeln!(
self.writer,
"<testcase classname=\"{class_name}\" \
name=\"{test_name}\" time=\"{elapsed_s}\"/>",
)?;
}
}
}
}
writeln!(self.writer, "<system-out/>")?;
writeln!(self.writer, "<system-err/>")?;
writeln!(self.writer, "</testsuite>")?;
writeln!(self.writer, "</testsuites>")?;
}
Ok(())
}
}

fn parse_class_name(name: &str) -> (String, String) {
// Module path => classname
// Function name => name
let module_segments: Vec<&str> = name.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)
}
4 changes: 4 additions & 0 deletions crates/libtest2-harness/src/notify/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "junit")]
mod junit;
mod pretty;
mod summary;
mod terse;

#[cfg(feature = "json")]
pub(crate) use json::*;
#[cfg(feature = "junit")]
pub(crate) use junit::*;
pub(crate) use pretty::*;
pub(crate) use summary::*;
pub(crate) use terse::*;
Expand Down
3 changes: 2 additions & 1 deletion crates/libtest2-mimic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ pre-release-replacements = [
]

[features]
default = ["json"]
default = ["json", "junit"]
json = ["libtest2-harness/json"]
junit = ["libtest2-harness/junit"]

[dependencies]
libtest2-harness = { version = "0.1.0", path = "../libtest2-harness" }
Expand Down
50 changes: 50 additions & 0 deletions crates/libtest2-mimic/tests/testsuite/mixed_bag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 6 filtered out; finished in
}

#[test]
#[cfg(feature = "json")]
fn list_json() {
check(
&["-Zunstable-options", "--format=json", "--list", "a"],
Expand Down Expand Up @@ -651,6 +652,7 @@ fn list_json() {
}

#[test]
#[cfg(feature = "json")]
fn test_json() {
check(
&["-Zunstable-options", "--format=json", "a"],
Expand Down Expand Up @@ -692,6 +694,54 @@ fn test_json() {
)
}

#[test]
#[cfg(feature = "junit")]
fn list_junit() {
check(
&["-Zunstable-options", "--format=junit", "--list", "a"],
0,
r#"bear: test
cat: test

2 tests

"#,
r#"bear: test
cat: test

2 tests

"#,
)
}

#[test]
#[cfg(feature = "junit")]
fn test_junit() {
check(
&["-Zunstable-options", "--format=junit", "a"],
0,
r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="test" package="test" id="0" tests="2" errors="0" failures="0" skipped="1" >
<testcase classname="crate" name="cat" time="0.000s"/>
<system-out/>
<system-err/>
</testsuite>
</testsuites>
"#,
r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="test" package="test" id="0" tests="2" errors="0" failures="0" skipped="1" >
<testcase classname="crate" name="cat" time="0.000s"/>
<system-out/>
<system-err/>
</testsuite>
</testsuites>
"#,
)
}

#[test]
fn terse_output() {
check(
Expand Down