Skip to content

Commit 610f827

Browse files
committed
junit: also include per-case stdout in xml
By placing the stdout in a CDATA block we avoid almost all escaping, as there's only two byte sequences you can't sneak into a CDATA and you can handle that with some only slightly regrettable CDATA-splitting. I've done this in at least two other implementations of the junit xml format over the years and it's always worked out. The only quirk new to this (for me) is smuggling newlines as 
 to avoid literal newlines in the output.
1 parent d77f636 commit 610f827

File tree

3 files changed

+37
-7
lines changed

3 files changed

+37
-7
lines changed

library/test/src/formatters/junit.rs

Lines changed: 35 additions & 5 deletions
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 line 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() {
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
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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"/></testcase><testcase classname="unknown" name="c" time="$TIME"/><system-out/><system-err/></testsuite></testsuites>
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>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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"/></testcase><testcase classname="unknown" name="c" time="$TIME"/><system-out/><system-err/></testsuite></testsuites>
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>

0 commit comments

Comments
 (0)