mirror of https://github.com/rust-lang/rust
223 lines
8.4 KiB
Rust
223 lines
8.4 KiB
Rust
use std::io::{self, prelude::Write};
|
|
use std::time::Duration;
|
|
|
|
use super::OutputFormatter;
|
|
use crate::{
|
|
console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation},
|
|
test_result::TestResult,
|
|
time,
|
|
types::{TestDesc, TestType},
|
|
};
|
|
|
|
pub struct JunitFormatter<T> {
|
|
out: OutputLocation<T>,
|
|
results: Vec<(TestDesc, TestResult, Duration, Vec<u8>)>,
|
|
}
|
|
|
|
impl<T: Write> JunitFormatter<T> {
|
|
pub fn new(out: OutputLocation<T>) -> Self {
|
|
Self { out, results: Vec::new() }
|
|
}
|
|
|
|
fn write_message(&mut self, s: &str) -> io::Result<()> {
|
|
assert!(!s.contains('\n'));
|
|
|
|
self.out.write_all(s.as_ref())
|
|
}
|
|
}
|
|
|
|
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 
 so as to keep all the output on one line
|
|
let escaped_output = escaped_output.replace('\n', "]]>
<![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!"))
|
|
}
|
|
|
|
fn write_test_discovered(&mut self, _desc: &TestDesc, _test_type: &str) -> io::Result<()> {
|
|
Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
|
|
}
|
|
|
|
fn write_discovery_finish(&mut self, _state: &ConsoleTestDiscoveryState) -> io::Result<()> {
|
|
Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!"))
|
|
}
|
|
|
|
fn write_run_start(
|
|
&mut self,
|
|
_test_count: usize,
|
|
_shuffle_seed: Option<u64>,
|
|
) -> io::Result<()> {
|
|
// We write xml header on run start
|
|
self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
|
|
}
|
|
|
|
fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
|
|
// We do not output anything on test start.
|
|
Ok(())
|
|
}
|
|
|
|
fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
|
|
// We do not output anything on test timeout.
|
|
Ok(())
|
|
}
|
|
|
|
fn write_result(
|
|
&mut self,
|
|
desc: &TestDesc,
|
|
result: &TestResult,
|
|
exec_time: Option<&time::TestExecTime>,
|
|
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, stdout.to_vec()));
|
|
Ok(())
|
|
}
|
|
fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
|
|
self.write_message("<testsuites>")?;
|
|
|
|
self.write_message(&format!(
|
|
"<testsuite name=\"test\" package=\"test\" id=\"0\" \
|
|
errors=\"0\" \
|
|
failures=\"{}\" \
|
|
tests=\"{}\" \
|
|
skipped=\"{}\" \
|
|
>",
|
|
state.failed, state.total, state.ignored
|
|
))?;
|
|
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 */ }
|
|
TestResult::TrFailed => {
|
|
self.write_message(&format!(
|
|
"<testcase classname=\"{}\" \
|
|
name=\"{}\" time=\"{}\">",
|
|
class_name,
|
|
test_name,
|
|
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>")?;
|
|
}
|
|
|
|
TestResult::TrFailedMsg(ref m) => {
|
|
self.write_message(&format!(
|
|
"<testcase classname=\"{}\" \
|
|
name=\"{}\" time=\"{}\">",
|
|
class_name,
|
|
test_name,
|
|
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>")?;
|
|
}
|
|
|
|
TestResult::TrTimedFail => {
|
|
self.write_message(&format!(
|
|
"<testcase classname=\"{}\" \
|
|
name=\"{}\" time=\"{}\">",
|
|
class_name,
|
|
test_name,
|
|
duration.as_secs_f64()
|
|
))?;
|
|
self.write_message("<failure type=\"timeout\"/>")?;
|
|
self.write_message("</testcase>")?;
|
|
}
|
|
|
|
TestResult::TrBench(ref b) => {
|
|
self.write_message(&format!(
|
|
"<testcase classname=\"benchmark::{}\" \
|
|
name=\"{}\" time=\"{}\" />",
|
|
class_name, test_name, b.ns_iter_summ.sum
|
|
))?;
|
|
}
|
|
|
|
TestResult::TrOk => {
|
|
self.write_message(&format!(
|
|
"<testcase classname=\"{}\" \
|
|
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("><system-out>")?;
|
|
self.write_message(&str_to_cdata(&String::from_utf8_lossy(&stdout)))?;
|
|
self.write_message("</system-out>")?;
|
|
self.write_message("</testcase>")?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.write_message("<system-out/>")?;
|
|
self.write_message("<system-err/>")?;
|
|
self.write_message("</testsuite>")?;
|
|
self.write_message("</testsuites>")?;
|
|
|
|
self.out.write_all(b"\n")?;
|
|
|
|
Ok(state.failed == 0)
|
|
}
|
|
}
|
|
|
|
fn parse_class_name(desc: &TestDesc) -> (String, String) {
|
|
match desc.test_type {
|
|
TestType::UnitTest => parse_class_name_unit(desc),
|
|
TestType::DocTest => parse_class_name_doc(desc),
|
|
TestType::IntegrationTest => parse_class_name_integration(desc),
|
|
TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())),
|
|
}
|
|
}
|
|
|
|
fn parse_class_name_unit(desc: &TestDesc) -> (String, String) {
|
|
// Module path => classname
|
|
// Function name => name
|
|
let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect();
|
|
let (class_name, test_name) = match module_segments[..] {
|
|
[test] => (String::from("crate"), String::from(test)),
|
|
[ref path @ .., test] => (path.join("::"), String::from(test)),
|
|
[..] => unreachable!(),
|
|
};
|
|
(class_name, test_name)
|
|
}
|
|
|
|
fn parse_class_name_doc(desc: &TestDesc) -> (String, String) {
|
|
// File path => classname
|
|
// Line # => test name
|
|
let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect();
|
|
let (class_name, test_name) = match segments[..] {
|
|
[file, line] => (String::from(file.trim()), String::from(line.trim())),
|
|
[..] => unreachable!(),
|
|
};
|
|
(class_name, test_name)
|
|
}
|
|
|
|
fn parse_class_name_integration(desc: &TestDesc) -> (String, String) {
|
|
(String::from("integration"), String::from(desc.name.as_slice()))
|
|
}
|