about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAugie Fackler <augie@google.com>2023-04-21 10:45:17 -0400
committerAugie Fackler <augie@google.com>2023-04-21 13:15:04 -0400
commit610f82726172c26d0a9f72fee3d72caad9bdb514 (patch)
tree45db417d15e169f1cb5ef6fa45831f4147189615
parentd77f636c6339d97ef3fa790f974d56b182cd59d1 (diff)
downloadrust-610f82726172c26d0a9f72fee3d72caad9bdb514.tar.gz
rust-610f82726172c26d0a9f72fee3d72caad9bdb514.zip
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 &#xA; to avoid literal newlines in the
output.
-rw-r--r--library/test/src/formatters/junit.rs40
-rw-r--r--tests/run-make/libtest-junit/output-default.xml2
-rw-r--r--tests/run-make/libtest-junit/output-stdout-success.xml2
3 files changed, 37 insertions, 7 deletions
diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs
index 2e07ce3c099..65883dd36a1 100644
--- a/library/test/src/formatters/junit.rs
+++ b/library/test/src/formatters/junit.rs
@@ -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> {
@@ -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!"))
@@ -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> {
@@ -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 */ }
@@ -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>")?;
                 }
 
@@ -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>")?;
                 }
 
@@ -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>")?;
+                    }
                 }
             }
         }
diff --git a/tests/run-make/libtest-junit/output-default.xml b/tests/run-make/libtest-junit/output-default.xml
index ea61562a33c..0c300611e1f 100644
--- a/tests/run-make/libtest-junit/output-default.xml
+++ b/tests/run-make/libtest-junit/output-default.xml
@@ -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>
diff --git a/tests/run-make/libtest-junit/output-stdout-success.xml b/tests/run-make/libtest-junit/output-stdout-success.xml
index ea61562a33c..0c300611e1f 100644
--- a/tests/run-make/libtest-junit/output-stdout-success.xml
+++ b/tests/run-make/libtest-junit/output-stdout-success.xml
@@ -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>