diff --git a/crates/libtest2-harness/Cargo.toml b/crates/libtest2-harness/Cargo.toml index 43052fd..4805b88 100644 --- a/crates/libtest2-harness/Cargo.toml +++ b/crates/libtest2-harness/Cargo.toml @@ -25,6 +25,7 @@ pre-release-replacements = [ [features] default = [] json = ["dep:serde", "dep:serde_json"] +junit = [] [dependencies] anstream = "0.3.1" diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index 9f91379..8ea4a35 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -126,12 +126,23 @@ fn notifier(opts: &libtest_lexarg::TestOpts) -> std::io::Result 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) } diff --git a/crates/libtest2-harness/src/notify/junit.rs b/crates/libtest2-harness/src/notify/junit.rs new file mode 100644 index 0000000..de8dbf7 --- /dev/null +++ b/crates/libtest2-harness/src/notify/junit.rs @@ -0,0 +1,121 @@ +use super::Event; +use super::RunStatus; + +#[derive(Debug)] +pub(crate) struct JunitRunNotifier { + writer: W, + events: Vec, +} + +impl JunitRunNotifier { + pub(crate) fn new(writer: W) -> Self { + Self { + writer, + events: Vec::new(), + } + } +} + +impl super::Notifier for JunitRunNotifier { + 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, "")?; + writeln!(self.writer, "")?; + + writeln!( + self.writer, + "" + )?; + 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, + "", + )?; + if let Some(message) = message { + writeln!( + self.writer, + "" + )?; + } else { + writeln!(self.writer, "")?; + } + writeln!(self.writer, "")?; + } + None => { + writeln!( + self.writer, + "", + )?; + } + } + } + } + writeln!(self.writer, "")?; + writeln!(self.writer, "")?; + writeln!(self.writer, "")?; + writeln!(self.writer, "")?; + } + 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) +} diff --git a/crates/libtest2-harness/src/notify/mod.rs b/crates/libtest2-harness/src/notify/mod.rs index 8c69997..bf8a70a 100644 --- a/crates/libtest2-harness/src/notify/mod.rs +++ b/crates/libtest2-harness/src/notify/mod.rs @@ -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::*; diff --git a/crates/libtest2-mimic/Cargo.toml b/crates/libtest2-mimic/Cargo.toml index ea35468..3a03e34 100644 --- a/crates/libtest2-mimic/Cargo.toml +++ b/crates/libtest2-mimic/Cargo.toml @@ -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" } diff --git a/crates/libtest2-mimic/tests/testsuite/mixed_bag.rs b/crates/libtest2-mimic/tests/testsuite/mixed_bag.rs index 9dcf446..9c8968f 100644 --- a/crates/libtest2-mimic/tests/testsuite/mixed_bag.rs +++ b/crates/libtest2-mimic/tests/testsuite/mixed_bag.rs @@ -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"], @@ -651,6 +652,7 @@ fn list_json() { } #[test] +#[cfg(feature = "json")] fn test_json() { check( &["-Zunstable-options", "--format=json", "a"], @@ -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#" + + + + + + + +"#, + r#" + + + + + + + +"#, + ) +} + #[test] fn terse_output() { check(