diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs index 2e07ce3c099..9f5bf24367e 100644 --- a/library/test/src/formatters/junit.rs +++ b/library/test/src/formatters/junit.rs @@ -11,7 +11,7 @@ pub struct JunitFormatter { out: OutputLocation, - results: Vec<(TestDesc, TestResult, Duration)>, + results: Vec<(TestDesc, TestResult, Duration, Vec)>, } impl JunitFormatter { @@ -26,6 +26,18 @@ fn write_message(&mut self, s: &str) -> io::Result<()> { } } +fn str_to_cdata(s: &str) -> String { + // Drop the stdout in a cdata. Unfortunately, you can't put either of `]]>` or + // `", "]]]]>"); + let escaped_output = escaped_output.replace(" ", ""); + format!("", escaped_output) +} + impl OutputFormatter for JunitFormatter { fn write_discovery_start(&mut self) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!")) @@ -63,14 +75,14 @@ fn write_result( desc: &TestDesc, result: &TestResult, exec_time: Option<&time::TestExecTime>, - _stdout: &[u8], + stdout: &[u8], _state: &ConsoleTestState, ) -> io::Result<()> { // Because the testsuite node holds some of the information as attributes, we can't write it // until all of the tests have finished. Instead of writing 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).unwrap_or_default(); - self.results.push((desc.clone(), result.clone(), duration)); + self.results.push((desc.clone(), result.clone(), duration, stdout.to_vec())); Ok(()) } fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { @@ -85,7 +97,7 @@ fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { >", state.failed, state.total, state.ignored ))?; - for (desc, result, duration) in std::mem::take(&mut self.results) { + for (desc, result, duration, stdout) in std::mem::take(&mut self.results) { let (class_name, test_name) = parse_class_name(&desc); match result { TestResult::TrIgnored => { /* no-op */ } @@ -98,6 +110,11 @@ fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { duration.as_secs_f64() ))?; self.write_message("")?; + if !stdout.is_empty() { + self.write_message("")?; + self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?; + self.write_message("")?; + } self.write_message("")?; } @@ -110,6 +127,11 @@ fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { duration.as_secs_f64() ))?; self.write_message(&format!(""))?; + if !stdout.is_empty() { + self.write_message("")?; + self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?; + self.write_message("")?; + } self.write_message("")?; } @@ -136,11 +158,19 @@ fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { TestResult::TrOk => { self.write_message(&format!( "", + name=\"{}\" time=\"{}\"", class_name, test_name, duration.as_secs_f64() ))?; + if stdout.is_empty() || !state.options.display_output { + self.write_message("/>")?; + } else { + self.write_message(">")?; + self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?; + self.write_message("")?; + self.write_message("")?; + } } } } diff --git a/tests/run-make/libtest-junit/Makefile b/tests/run-make/libtest-junit/Makefile new file mode 100644 index 00000000000..d97cafccf1f --- /dev/null +++ b/tests/run-make/libtest-junit/Makefile @@ -0,0 +1,19 @@ +# ignore-cross-compile +include ../tools.mk + +# Test expected libtest's junit output + +OUTPUT_FILE_DEFAULT := $(TMPDIR)/libtest-junit-output-default.xml +OUTPUT_FILE_STDOUT_SUCCESS := $(TMPDIR)/libtest-junit-output-stdout-success.xml + +all: f.rs validate_junit.py output-default.xml output-stdout-success.xml + $(RUSTC) --test f.rs + RUST_BACKTRACE=0 $(call RUN,f) -Z unstable-options --test-threads=1 --format=junit > $(OUTPUT_FILE_DEFAULT) || true + RUST_BACKTRACE=0 $(call RUN,f) -Z unstable-options --test-threads=1 --format=junit --show-output > $(OUTPUT_FILE_STDOUT_SUCCESS) || true + + cat $(OUTPUT_FILE_DEFAULT) | "$(PYTHON)" validate_junit.py + cat $(OUTPUT_FILE_STDOUT_SUCCESS) | "$(PYTHON)" validate_junit.py + + # Normalize the actual output and compare to expected output file + cat $(OUTPUT_FILE_DEFAULT) | sed 's/time="[0-9.]*"/time="$$TIME"/g' | diff output-default.xml - + cat $(OUTPUT_FILE_STDOUT_SUCCESS) | sed 's/time="[0-9.]*"/time="$$TIME"/g' | diff output-stdout-success.xml - diff --git a/tests/run-make/libtest-junit/f.rs b/tests/run-make/libtest-junit/f.rs new file mode 100644 index 00000000000..d360d77317d --- /dev/null +++ b/tests/run-make/libtest-junit/f.rs @@ -0,0 +1,23 @@ +#[test] +fn a() { + println!("print from successful test"); + // Should pass +} + +#[test] +fn b() { + println!("print from failing test"); + assert!(false); +} + +#[test] +#[should_panic] +fn c() { + assert!(false); +} + +#[test] +#[ignore = "msg"] +fn d() { + assert!(false); +} diff --git a/tests/run-make/libtest-junit/output-default.xml b/tests/run-make/libtest-junit/output-default.xml new file mode 100644 index 00000000000..d59e07b8ad8 --- /dev/null +++ b/tests/run-make/libtest-junit/output-default.xml @@ -0,0 +1 @@ + diff --git a/tests/run-make/libtest-junit/output-stdout-success.xml b/tests/run-make/libtest-junit/output-stdout-success.xml new file mode 100644 index 00000000000..0c300611e1f --- /dev/null +++ b/tests/run-make/libtest-junit/output-stdout-success.xml @@ -0,0 +1 @@ + diff --git a/tests/run-make/libtest-junit/validate_junit.py b/tests/run-make/libtest-junit/validate_junit.py new file mode 100755 index 00000000000..47a8e70ccc3 --- /dev/null +++ b/tests/run-make/libtest-junit/validate_junit.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import sys +import xml.etree.ElementTree as ET + +# Try to decode line in order to ensure it is a valid XML document +for line in sys.stdin: + try: + ET.fromstring(line) + except ET.ParseError as pe: + print("Invalid xml: %r" % line) + raise