Skip to content

Commit 7dcf6c5

Browse files
committed
Introduce test result output file option for libtest
Separating test results from other output **avoids contamination**. If `libtest` only writes output to stdout, any non-test output (log messages, debug prints, etc.) may corrupt the stream and break parsers. Rust’s `println`’s are wrapped by libtest, but anything can (and does, in real world) use `libc`, or have C code using `libc` that corrupts stdout. Also, in practice, projects often resort to external post-processing to filter test output. As one [tracking discussion notes](rust-lang#85563), *“due to limitations of Rust libtest formatters, Rust developers often use a separate tool to postprocess the test results output”*. By writing test results directly to a file, we can guarantee the test results are isolated and parseable, without third-party noise.
1 parent efcbb94 commit 7dcf6c5

File tree

4 files changed

+59
-10
lines changed

4 files changed

+59
-10
lines changed

library/test/src/cli.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct TestOpts {
1818
pub run_tests: bool,
1919
pub bench_benchmarks: bool,
2020
pub logfile: Option<PathBuf>,
21+
pub test_results_file: Option<PathBuf>,
2122
pub nocapture: bool,
2223
pub color: ColorConfig,
2324
pub format: OutputFormat,
@@ -59,6 +60,7 @@ fn optgroups() -> getopts::Options {
5960
.optflag("", "list", "List all tests and benchmarks")
6061
.optflag("h", "help", "Display this message")
6162
.optopt("", "logfile", "Write logs to the specified file (deprecated)", "PATH")
63+
.optopt("", "test-results-file", "Write test results to the specified file", "PATH")
6264
.optflag(
6365
"",
6466
"no-capture",
@@ -275,6 +277,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
275277
let run_tests = !bench_benchmarks || matches.opt_present("test");
276278

277279
let logfile = get_log_file(&matches)?;
280+
let test_results_file = get_test_results_file(&matches)?;
278281
let run_ignored = get_run_ignored(&matches, include_ignored)?;
279282
let filters = matches.free.clone();
280283
let nocapture = get_nocapture(&matches)?;
@@ -298,6 +301,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
298301
run_tests,
299302
bench_benchmarks,
300303
logfile,
304+
test_results_file,
301305
nocapture,
302306
color,
303307
format,
@@ -500,3 +504,9 @@ fn get_log_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> {
500504

501505
Ok(logfile)
502506
}
507+
508+
fn get_test_results_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> {
509+
let test_results_file = matches.opt_str("test-results-file").map(|s| PathBuf::from(&s));
510+
511+
Ok(test_results_file)
512+
}

library/test/src/console.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Module providing interface for running tests in the console.
22
3-
use std::fs::File;
3+
use std::fs::{File, OpenOptions};
44
use std::io;
55
use std::io::prelude::Write;
6+
use std::path::PathBuf;
67
use std::time::Instant;
78

89
use super::bench::fmt_bench_samples;
@@ -171,11 +172,7 @@ impl ConsoleTestState {
171172

172173
// List the tests to console, and optionally to logfile. Filters are honored.
173174
pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<()> {
174-
let output = match term::stdout() {
175-
None => OutputLocation::Raw(io::stdout().lock()),
176-
Some(t) => OutputLocation::Pretty(t),
177-
};
178-
175+
let output = build_output(&opts.test_results_file)?;
179176
let mut out: Box<dyn OutputFormatter> = match opts.format {
180177
OutputFormat::Pretty | OutputFormat::Junit => {
181178
Box::new(PrettyFormatter::new(output, false, 0, false, None))
@@ -211,6 +208,24 @@ pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) ->
211208
out.write_discovery_finish(&st)
212209
}
213210

211+
pub(crate) fn build_output(
212+
test_results_file: &Option<PathBuf>,
213+
) -> io::Result<OutputLocation<Box<dyn Write>>> {
214+
let output: OutputLocation<Box<dyn Write>> = match test_results_file {
215+
Some(results_file_path) => {
216+
let file_output =
217+
OpenOptions::new().write(true).create_new(true).open(results_file_path)?;
218+
219+
OutputLocation::Raw(Box::new(file_output))
220+
}
221+
None => match term::stdout() {
222+
None => OutputLocation::Raw(Box::new(io::stdout().lock())),
223+
Some(t) => OutputLocation::Pretty(t),
224+
},
225+
};
226+
Ok(output)
227+
}
228+
214229
// Updates `ConsoleTestState` depending on result of the test execution.
215230
fn handle_test_result(st: &mut ConsoleTestState, completed_test: CompletedTest) {
216231
let test = completed_test.desc;
@@ -284,10 +299,7 @@ fn on_test_event(
284299
/// A simple console test runner.
285300
/// Runs provided tests reporting process and results to the stdout.
286301
pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<bool> {
287-
let output = match term::stdout() {
288-
None => OutputLocation::Raw(io::stdout()),
289-
Some(t) => OutputLocation::Pretty(t),
290-
};
302+
let output = build_output(&opts.test_results_file)?;
291303

292304
let max_name_len = tests
293305
.iter()

library/test/src/tests.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::PathBuf;
2+
13
use super::*;
24
use crate::{
35
console::OutputLocation,
@@ -24,6 +26,7 @@ impl TestOpts {
2426
run_tests: false,
2527
bench_benchmarks: false,
2628
logfile: None,
29+
test_results_file: None,
2730
nocapture: false,
2831
color: AutoColor,
2932
format: OutputFormat::Pretty,
@@ -468,6 +471,29 @@ fn parse_include_ignored_flag() {
468471
assert_eq!(opts.run_ignored, RunIgnored::Yes);
469472
}
470473

474+
#[test]
475+
fn parse_test_results_file_flag_reads_value() {
476+
let args = vec![
477+
"progname".to_string(),
478+
"filter".to_string(),
479+
"--test-results-file".to_string(),
480+
"expected_path_to_results_file".to_string(),
481+
];
482+
let opts = parse_opts(&args).unwrap().unwrap();
483+
assert_eq!(opts.test_results_file, Some(PathBuf::from("expected_path_to_results_file")));
484+
}
485+
486+
#[test]
487+
fn parse_test_results_file_requires_value() {
488+
let args =
489+
vec!["progname".to_string(), "filter".to_string(), "--test-results-file".to_string()];
490+
let maybe_opts = parse_opts(&args).unwrap();
491+
assert_eq!(
492+
maybe_opts.err(),
493+
Some("Argument to option 'test-results-file' missing".to_string())
494+
);
495+
}
496+
471497
#[test]
472498
fn filter_for_ignored_option() {
473499
// When we run ignored tests the test filter should filter out all the

src/tools/compiletest/src/executor/libtest.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ fn test_opts(config: &Config) -> test::TestOpts {
9494
run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
9595
format: config.format.to_libtest(),
9696
logfile: None,
97+
test_results_file: None,
9798
run_tests: true,
9899
bench_benchmarks: true,
99100
nocapture: config.nocapture,

0 commit comments

Comments
 (0)