Skip to content

Commit 35bc61e

Browse files
authored
Rollup merge of rust-lang#110651 - durin42:xunit-stdout, r=cuviper
libtest: include test output in junit xml reports Fixes rust-lang#110336.
2 parents 4af1284 + 58537cd commit 35bc61e

File tree

6 files changed

+91
-5
lines changed

6 files changed

+91
-5
lines changed

library/test/src/formatters/junit.rs

+35-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111

1212
pub struct JunitFormatter<T> {
1313
out: OutputLocation<T>,
14-
results: Vec<(TestDesc, TestResult, Duration)>,
14+
results: Vec<(TestDesc, TestResult, Duration, Vec<u8>)>,
1515
}
1616

1717
impl<T: Write> JunitFormatter<T> {
@@ -26,6 +26,18 @@ impl<T: Write> JunitFormatter<T> {
2626
}
2727
}
2828

29+
fn str_to_cdata(s: &str) -> String {
30+
// Drop the stdout in a cdata. Unfortunately, you can't put either of `]]>` or
31+
// `<?'` in a CDATA block, so the escaping gets a little weird.
32+
let escaped_output = s.replace("]]>", "]]]]><![CDATA[>");
33+
let escaped_output = escaped_output.replace("<?", "<]]><![CDATA[?");
34+
// We also smuggle newlines as &#xa so as to keep all the output on one line
35+
let escaped_output = escaped_output.replace("\n", "]]>&#xA;<![CDATA[");
36+
// Prune empty CDATA blocks resulting from any escaping
37+
let escaped_output = escaped_output.replace("<![CDATA[]]>", "");
38+
format!("<![CDATA[{}]]>", escaped_output)
39+
}
40+
2941
impl<T: Write> OutputFormatter for JunitFormatter<T> {
3042
fn write_discovery_start(&mut self) -> io::Result<()> {
3143
Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
@@ -63,14 +75,14 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
6375
desc: &TestDesc,
6476
result: &TestResult,
6577
exec_time: Option<&time::TestExecTime>,
66-
_stdout: &[u8],
78+
stdout: &[u8],
6779
_state: &ConsoleTestState,
6880
) -> io::Result<()> {
6981
// Because the testsuite node holds some of the information as attributes, we can't write it
7082
// until all of the tests have finished. Instead of writing every result as they come in, we add
7183
// them to a Vec and write them all at once when run is complete.
7284
let duration = exec_time.map(|t| t.0).unwrap_or_default();
73-
self.results.push((desc.clone(), result.clone(), duration));
85+
self.results.push((desc.clone(), result.clone(), duration, stdout.to_vec()));
7486
Ok(())
7587
}
7688
fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
@@ -85,7 +97,7 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
8597
>",
8698
state.failed, state.total, state.ignored
8799
))?;
88-
for (desc, result, duration) in std::mem::take(&mut self.results) {
100+
for (desc, result, duration, stdout) in std::mem::take(&mut self.results) {
89101
let (class_name, test_name) = parse_class_name(&desc);
90102
match result {
91103
TestResult::TrIgnored => { /* no-op */ }
@@ -98,6 +110,11 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
98110
duration.as_secs_f64()
99111
))?;
100112
self.write_message("<failure type=\"assert\"/>")?;
113+
if !stdout.is_empty() {
114+
self.write_message("<system-out>")?;
115+
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
116+
self.write_message("</system-out>")?;
117+
}
101118
self.write_message("</testcase>")?;
102119
}
103120

@@ -110,6 +127,11 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
110127
duration.as_secs_f64()
111128
))?;
112129
self.write_message(&format!("<failure message=\"{m}\" type=\"assert\"/>"))?;
130+
if !stdout.is_empty() {
131+
self.write_message("<system-out>")?;
132+
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
133+
self.write_message("</system-out>")?;
134+
}
113135
self.write_message("</testcase>")?;
114136
}
115137

@@ -136,11 +158,19 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
136158
TestResult::TrOk => {
137159
self.write_message(&format!(
138160
"<testcase classname=\"{}\" \
139-
name=\"{}\" time=\"{}\"/>",
161+
name=\"{}\" time=\"{}\"",
140162
class_name,
141163
test_name,
142164
duration.as_secs_f64()
143165
))?;
166+
if stdout.is_empty() || !state.options.display_output {
167+
self.write_message("/>")?;
168+
} else {
169+
self.write_message("><system-out>")?;
170+
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
171+
self.write_message("</system-out>")?;
172+
self.write_message("</testcase>")?;
173+
}
144174
}
145175
}
146176
}

tests/run-make/libtest-junit/Makefile

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ignore-cross-compile
2+
include ../tools.mk
3+
4+
# Test expected libtest's junit output
5+
6+
OUTPUT_FILE_DEFAULT := $(TMPDIR)/libtest-junit-output-default.xml
7+
OUTPUT_FILE_STDOUT_SUCCESS := $(TMPDIR)/libtest-junit-output-stdout-success.xml
8+
9+
all: f.rs validate_junit.py output-default.xml output-stdout-success.xml
10+
$(RUSTC) --test f.rs
11+
RUST_BACKTRACE=0 $(call RUN,f) -Z unstable-options --test-threads=1 --format=junit > $(OUTPUT_FILE_DEFAULT) || true
12+
RUST_BACKTRACE=0 $(call RUN,f) -Z unstable-options --test-threads=1 --format=junit --show-output > $(OUTPUT_FILE_STDOUT_SUCCESS) || true
13+
14+
cat $(OUTPUT_FILE_DEFAULT) | "$(PYTHON)" validate_junit.py
15+
cat $(OUTPUT_FILE_STDOUT_SUCCESS) | "$(PYTHON)" validate_junit.py
16+
17+
# Normalize the actual output and compare to expected output file
18+
cat $(OUTPUT_FILE_DEFAULT) | sed 's/time="[0-9.]*"/time="$$TIME"/g' | diff output-default.xml -
19+
cat $(OUTPUT_FILE_STDOUT_SUCCESS) | sed 's/time="[0-9.]*"/time="$$TIME"/g' | diff output-stdout-success.xml -

tests/run-make/libtest-junit/f.rs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#[test]
2+
fn a() {
3+
println!("print from successful test");
4+
// Should pass
5+
}
6+
7+
#[test]
8+
fn b() {
9+
println!("print from failing test");
10+
assert!(false);
11+
}
12+
13+
#[test]
14+
#[should_panic]
15+
fn c() {
16+
assert!(false);
17+
}
18+
19+
#[test]
20+
#[ignore = "msg"]
21+
fn d() {
22+
assert!(false);
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?xml version="1.0" encoding="UTF-8"?><testsuites><testsuite name="test" package="test" id="0" errors="0" failures="1" tests="4" skipped="1" ><testcase classname="unknown" name="a" time="$TIME"/><testcase classname="unknown" name="b" time="$TIME"><failure type="assert"/><system-out><![CDATA[print from failing test]]>&#xA;<![CDATA[thread 'b' panicked at 'assertion failed: false', f.rs:10:5]]>&#xA;<![CDATA[note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace]]>&#xA;<![CDATA[]]></system-out></testcase><testcase classname="unknown" name="c" time="$TIME"/><system-out/><system-err/></testsuite></testsuites>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?xml version="1.0" encoding="UTF-8"?><testsuites><testsuite name="test" package="test" id="0" errors="0" failures="1" tests="4" skipped="1" ><testcase classname="unknown" name="a" time="$TIME"><system-out><![CDATA[print from successful test]]>&#xA;<![CDATA[]]></system-out></testcase><testcase classname="unknown" name="b" time="$TIME"><failure type="assert"/><system-out><![CDATA[print from failing test]]>&#xA;<![CDATA[thread 'b' panicked at 'assertion failed: false', f.rs:10:5]]>&#xA;<![CDATA[note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace]]>&#xA;<![CDATA[]]></system-out></testcase><testcase classname="unknown" name="c" time="$TIME"><system-out><![CDATA[thread 'c' panicked at 'assertion failed: false', f.rs:16:5]]>&#xA;<![CDATA[]]></system-out></testcase><system-out/><system-err/></testsuite></testsuites>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
import xml.etree.ElementTree as ET
5+
6+
# Try to decode line in order to ensure it is a valid XML document
7+
for line in sys.stdin:
8+
try:
9+
ET.fromstring(line)
10+
except ET.ParseError as pe:
11+
print("Invalid xml: %r" % line)
12+
raise

0 commit comments

Comments
 (0)