Skip to content

Commit

Permalink
junit: also include per-case stdout in xml
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
durin42 committed Apr 21, 2023
1 parent d77f636 commit 610f827
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 7 deletions.
40 changes: 35 additions & 5 deletions library/test/src/formatters/junit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{

pub struct JunitFormatter<T> {
out: OutputLocation<T>,
results: Vec<(TestDesc, TestResult, Duration)>,
results: Vec<(TestDesc, TestResult, Duration, Vec<u8>)>,
}

impl<T: Write> JunitFormatter<T> {
Expand All @@ -26,6 +26,18 @@ impl<T: Write> JunitFormatter<T> {
}
}

fn str_to_cdata(s: &str) -> String {
// Drop the stdout in a cdata. Unfortunately, you can't put either of `]]>` or
// `<?'` in a CDATA block, so the escaping gets a little weird.
let escaped_output = s.replace("]]>", "]]]]><![CDATA[>");
let escaped_output = escaped_output.replace("<?", "<]]><![CDATA[?");
// We also smuggle newlines as &#xa so as to keep all the output on line line
let escaped_output = escaped_output.replace("\n", "]]>&#xA;<![CDATA[");
// Prune empty CDATA blocks resulting from any escaping
let escaped_output = escaped_output.replace("<![CDATA[]]>", "");
format!("<![CDATA[{}]]>", escaped_output)
}

impl<T: Write> OutputFormatter for JunitFormatter<T> {
fn write_discovery_start(&mut self) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
Expand Down Expand Up @@ -63,14 +75,14 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
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<bool> {
Expand All @@ -85,7 +97,7 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
>",
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 */ }
Expand All @@ -98,6 +110,11 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
duration.as_secs_f64()
))?;
self.write_message("<failure type=\"assert\"/>")?;
if !stdout.is_empty() {
self.write_message("<system-out>")?;
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
self.write_message("</system-out>")?;
}
self.write_message("</testcase>")?;
}

Expand All @@ -110,6 +127,11 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
duration.as_secs_f64()
))?;
self.write_message(&format!("<failure message=\"{m}\" type=\"assert\"/>"))?;
if !stdout.is_empty() {
self.write_message("<system-out>")?;
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
self.write_message("</system-out>")?;
}
self.write_message("</testcase>")?;
}

Expand All @@ -136,11 +158,19 @@ impl<T: Write> OutputFormatter for JunitFormatter<T> {
TestResult::TrOk => {
self.write_message(&format!(
"<testcase classname=\"{}\" \
name=\"{}\" time=\"{}\"/>",
name=\"{}\" time=\"{}\"",
class_name,
test_name,
duration.as_secs_f64()
))?;
if stdout.is_empty() {
self.write_message("/>")?;
} else {
self.write_message("><system-out>")?;
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
self.write_message("</system-out>")?;
self.write_message("</testcase>")?;
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/run-make/libtest-junit/output-default.xml
Original file line number Diff line number Diff line change
@@ -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>
<?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>
2 changes: 1 addition & 1 deletion tests/run-make/libtest-junit/output-stdout-success.xml
Original file line number Diff line number Diff line change
@@ -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>
<?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 comments on commit 610f827

Please sign in to comment.