Skip to content

Commit 1c6868a

Browse files
committed
Auto merge of #84568 - andoriyu:libtest/junit_formatter, r=yaahc
feat(libtest): Add JUnit formatter tracking issue: #85563 Add an alternative formatter to `libtest`. Formatter produces valid xml that later can be interpreted as JUnit report. Caveats: - `timestamp` is required by schema, but every viewer/parser ignores it. Attribute is not set to avoid depending on chrono; - Running all "suits" (unit tests, doc-tests and integration tests) will produce a mess; - I couldn't find a way to get integration test binary name, so it's just goes by "integration"; Sample output for unit tests (pretty printed by 3rd party tool): ``` <?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="test" package="test" id="0" errors="0" failures="0" tests="13" skipped="1"> <testcase classname="results::tests" name="test_completed_bad" time="0"/> <testcase classname="results::tests" name="suite_started" time="0"/> <testcase classname="results::tests" name="suite_ended_ok" time="0"/> <testcase classname="results::tests" name="suite_ended_bad" time="0"/> <testcase classname="junit::tests" name="test_failed_output" time="0"/> <testcase classname="junit::tests" name="test_simple_output" time="0"/> <testcase classname="junit::tests" name="test_multiple_outputs" time="0"/> <testcase classname="results::tests" name="test_completed_ok" time="0"/> <testcase classname="results::tests" name="test_stared" time="0"/> <testcase classname="junit::tests" name="test_generate_xml_no_error_single_testsuite" time="0"/> <testcase classname="results::tests" name="test_simple_output" time="0"/> <testcase classname="test" name="should_panic" time="0"/> <system-out/> <system-err/> </testsuite> </testsuites> ``` Sample output for integration tests (pretty printed by 3rd party tool): ``` <?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="test" package="test" id="0" errors="0" failures="0" tests="1" skipped="0"> <testcase classname="integration" name="test_add" time="0"/> <system-out/> <system-err/> </testsuite> </testsuites> ``` Sample output for Doc-tests (pretty printed by 3rd party tool): ``` <?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="test" package="test" id="0" errors="0" failures="0" tests="1" skipped="0"> <testcase classname="src/lib.rs" name="(line 2)" time="0"/> <system-out/> <system-err/> </testsuite> </testsuites> ```
2 parents e51830b + 9f83e22 commit 1c6868a

File tree

5 files changed

+190
-5
lines changed

5 files changed

+190
-5
lines changed

library/test/src/cli.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ fn optgroups() -> getopts::Options {
9595
"Configure formatting of output:
9696
pretty = Print verbose output;
9797
terse = Display one character per test;
98-
json = Output a json document",
99-
"pretty|terse|json",
98+
json = Output a json document;
99+
junit = Output a JUnit document",
100+
"pretty|terse|json|junit",
100101
)
101102
.optflag("", "show-output", "Show captured stdout of successful tests")
102103
.optopt(
@@ -336,10 +337,15 @@ fn get_format(
336337
}
337338
OutputFormat::Json
338339
}
339-
340+
Some("junit") => {
341+
if !allow_unstable {
342+
return Err("The \"junit\" format is only accepted on the nightly compiler".into());
343+
}
344+
OutputFormat::Junit
345+
}
340346
Some(v) => {
341347
return Err(format!(
342-
"argument for --format must be pretty, terse, or json (was \
348+
"argument for --format must be pretty, terse, json or junit (was \
343349
{})",
344350
v
345351
));

library/test/src/console.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use super::{
1010
cli::TestOpts,
1111
event::{CompletedTest, TestEvent},
1212
filter_tests,
13-
formatters::{JsonFormatter, OutputFormatter, PrettyFormatter, TerseFormatter},
13+
formatters::{JsonFormatter, JunitFormatter, OutputFormatter, PrettyFormatter, TerseFormatter},
1414
helpers::{concurrency::get_concurrency, metrics::MetricMap},
1515
options::{Options, OutputFormat},
1616
run_tests,
@@ -277,6 +277,7 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Resu
277277
Box::new(TerseFormatter::new(output, opts.use_color(), max_name_len, is_multithreaded))
278278
}
279279
OutputFormat::Json => Box::new(JsonFormatter::new(output)),
280+
OutputFormat::Junit => Box::new(JunitFormatter::new(output)),
280281
};
281282
let mut st = ConsoleTestState::new(opts)?;
282283

library/test/src/formatters/junit.rs

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use std::io::{self, prelude::Write};
2+
use std::time::Duration;
3+
4+
use super::OutputFormatter;
5+
use crate::{
6+
console::{ConsoleTestState, OutputLocation},
7+
test_result::TestResult,
8+
time,
9+
types::{TestDesc, TestType},
10+
};
11+
12+
pub struct JunitFormatter<T> {
13+
out: OutputLocation<T>,
14+
results: Vec<(TestDesc, TestResult, Duration)>,
15+
}
16+
17+
impl<T: Write> JunitFormatter<T> {
18+
pub fn new(out: OutputLocation<T>) -> Self {
19+
Self { out, results: Vec::new() }
20+
}
21+
22+
fn write_message(&mut self, s: &str) -> io::Result<()> {
23+
assert!(!s.contains('\n'));
24+
25+
self.out.write_all(s.as_ref())
26+
}
27+
}
28+
29+
impl<T: Write> OutputFormatter for JunitFormatter<T> {
30+
fn write_run_start(&mut self, _test_count: usize) -> io::Result<()> {
31+
// We write xml header on run start
32+
self.write_message(&"<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
33+
}
34+
35+
fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> {
36+
// We do not output anything on test start.
37+
Ok(())
38+
}
39+
40+
fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> {
41+
// We do not output anything on test timeout.
42+
Ok(())
43+
}
44+
45+
fn write_result(
46+
&mut self,
47+
desc: &TestDesc,
48+
result: &TestResult,
49+
exec_time: Option<&time::TestExecTime>,
50+
_stdout: &[u8],
51+
_state: &ConsoleTestState,
52+
) -> io::Result<()> {
53+
// Because the testsuit node holds some of the information as attributes, we can't write it
54+
// until all of the tests has ran. Instead of writting every result as they come in, we add
55+
// them to a Vec and write them all at once when run is complete.
56+
let duration = exec_time.map(|t| t.0.clone()).unwrap_or_default();
57+
self.results.push((desc.clone(), result.clone(), duration));
58+
Ok(())
59+
}
60+
fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
61+
self.write_message("<testsuites>")?;
62+
63+
self.write_message(&*format!(
64+
"<testsuite name=\"test\" package=\"test\" id=\"0\" \
65+
errors=\"0\" \
66+
failures=\"{}\" \
67+
tests=\"{}\" \
68+
skipped=\"{}\" \
69+
>",
70+
state.failed, state.total, state.ignored
71+
))?;
72+
for (desc, result, duration) in std::mem::replace(&mut self.results, Vec::new()) {
73+
let (class_name, test_name) = parse_class_name(&desc);
74+
match result {
75+
TestResult::TrIgnored => { /* no-op */ }
76+
TestResult::TrFailed => {
77+
self.write_message(&*format!(
78+
"<testcase classname=\"{}\" \
79+
name=\"{}\" time=\"{}\">",
80+
class_name,
81+
test_name,
82+
duration.as_secs()
83+
))?;
84+
self.write_message("<failure type=\"assert\"/>")?;
85+
self.write_message("</testcase>")?;
86+
}
87+
88+
TestResult::TrFailedMsg(ref m) => {
89+
self.write_message(&*format!(
90+
"<testcase classname=\"{}\" \
91+
name=\"{}\" time=\"{}\">",
92+
class_name,
93+
test_name,
94+
duration.as_secs()
95+
))?;
96+
self.write_message(&*format!("<failure message=\"{}\" type=\"assert\"/>", m))?;
97+
self.write_message("</testcase>")?;
98+
}
99+
100+
TestResult::TrTimedFail => {
101+
self.write_message(&*format!(
102+
"<testcase classname=\"{}\" \
103+
name=\"{}\" time=\"{}\">",
104+
class_name,
105+
test_name,
106+
duration.as_secs()
107+
))?;
108+
self.write_message("<failure type=\"timeout\"/>")?;
109+
self.write_message("</testcase>")?;
110+
}
111+
112+
TestResult::TrBench(ref b) => {
113+
self.write_message(&*format!(
114+
"<testcase classname=\"benchmark::{}\" \
115+
name=\"{}\" time=\"{}\" />",
116+
class_name, test_name, b.ns_iter_summ.sum
117+
))?;
118+
}
119+
120+
TestResult::TrOk | TestResult::TrAllowedFail => {
121+
self.write_message(&*format!(
122+
"<testcase classname=\"{}\" \
123+
name=\"{}\" time=\"{}\"/>",
124+
class_name,
125+
test_name,
126+
duration.as_secs()
127+
))?;
128+
}
129+
}
130+
}
131+
self.write_message("<system-out/>")?;
132+
self.write_message("<system-err/>")?;
133+
self.write_message("</testsuite>")?;
134+
self.write_message("</testsuites>")?;
135+
136+
Ok(state.failed == 0)
137+
}
138+
}
139+
140+
fn parse_class_name(desc: &TestDesc) -> (String, String) {
141+
match desc.test_type {
142+
TestType::UnitTest => parse_class_name_unit(desc),
143+
TestType::DocTest => parse_class_name_doc(desc),
144+
TestType::IntegrationTest => parse_class_name_integration(desc),
145+
TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())),
146+
}
147+
}
148+
149+
fn parse_class_name_unit(desc: &TestDesc) -> (String, String) {
150+
// Module path => classname
151+
// Function name => name
152+
let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect();
153+
let (class_name, test_name) = match module_segments[..] {
154+
[test] => (String::from("crate"), String::from(test)),
155+
[ref path @ .., test] => (path.join("::"), String::from(test)),
156+
[..] => unreachable!(),
157+
};
158+
(class_name, test_name)
159+
}
160+
161+
fn parse_class_name_doc(desc: &TestDesc) -> (String, String) {
162+
// File path => classname
163+
// Line # => test name
164+
let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect();
165+
let (class_name, test_name) = match segments[..] {
166+
[file, line] => (String::from(file.trim()), String::from(line.trim())),
167+
[..] => unreachable!(),
168+
};
169+
(class_name, test_name)
170+
}
171+
172+
fn parse_class_name_integration(desc: &TestDesc) -> (String, String) {
173+
(String::from("integration"), String::from(desc.name.as_slice()))
174+
}

library/test/src/formatters/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use crate::{
88
};
99

1010
mod json;
11+
mod junit;
1112
mod pretty;
1213
mod terse;
1314

1415
pub(crate) use self::json::JsonFormatter;
16+
pub(crate) use self::junit::JunitFormatter;
1517
pub(crate) use self::pretty::PrettyFormatter;
1618
pub(crate) use self::terse::TerseFormatter;
1719

library/test/src/options.rs

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub enum OutputFormat {
3939
Terse,
4040
/// JSON output
4141
Json,
42+
/// JUnit output
43+
Junit,
4244
}
4345

4446
/// Whether ignored test should be run or not

0 commit comments

Comments
 (0)