Skip to content

Introduce test results file #140805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions library/Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ dependencies = [
"core",
"getopts",
"libc",
"rand",
"rand_xorshift",
"std",
]

Expand Down
5 changes: 5 additions & 0 deletions library/test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ getopts = { version = "0.2.21", features = ['rustc-dep-of-std'] }
std = { path = "../std", public = true }
core = { path = "../core", public = true }

[dev-dependencies]
rand = { version = "0.9.0", default-features = false, features = ["alloc"] }
rand_xorshift = "0.4.0"


[target.'cfg(not(all(windows, target_env = "msvc")))'.dependencies]
libc = { version = "0.2.150", default-features = false }
10 changes: 10 additions & 0 deletions library/test/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct TestOpts {
pub run_tests: bool,
pub bench_benchmarks: bool,
pub logfile: Option<PathBuf>,
pub test_results_file: Option<PathBuf>,
pub nocapture: bool,
pub color: ColorConfig,
pub format: OutputFormat,
Expand Down Expand Up @@ -59,6 +60,7 @@ fn optgroups() -> getopts::Options {
.optflag("", "list", "List all tests and benchmarks")
.optflag("h", "help", "Display this message")
.optopt("", "logfile", "Write logs to the specified file (deprecated)", "PATH")
.optopt("", "test-results-file", "Write test results to the specified file", "PATH")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.optflag(
"",
"no-capture",
Expand Down Expand Up @@ -275,6 +277,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
let run_tests = !bench_benchmarks || matches.opt_present("test");

let logfile = get_log_file(&matches)?;
let test_results_file = get_test_results_file(&matches)?;
let run_ignored = get_run_ignored(&matches, include_ignored)?;
let filters = matches.free.clone();
let nocapture = get_nocapture(&matches)?;
Expand All @@ -298,6 +301,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
run_tests,
bench_benchmarks,
logfile,
test_results_file,
nocapture,
color,
format,
Expand Down Expand Up @@ -500,3 +504,9 @@ fn get_log_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> {

Ok(logfile)
}

fn get_test_results_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> {
let test_results_file = matches.opt_str("test-results-file").map(|s| PathBuf::from(&s));

Ok(test_results_file)
}
32 changes: 22 additions & 10 deletions library/test/src/console.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Module providing interface for running tests in the console.

use std::fs::File;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::Write;
use std::path::PathBuf;
use std::time::Instant;

use super::bench::fmt_bench_samples;
Expand Down Expand Up @@ -171,11 +172,7 @@ impl ConsoleTestState {

// List the tests to console, and optionally to logfile. Filters are honored.
pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<()> {
let output = match term::stdout() {
None => OutputLocation::Raw(io::stdout().lock()),
Some(t) => OutputLocation::Pretty(t),
};

let output = build_test_output(&opts.test_results_file)?;
let mut out: Box<dyn OutputFormatter> = match opts.format {
OutputFormat::Pretty | OutputFormat::Junit => {
Box::new(PrettyFormatter::new(output, false, 0, false, None))
Expand Down Expand Up @@ -211,6 +208,24 @@ pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) ->
out.write_discovery_finish(&st)
}

pub(crate) fn build_test_output(
test_results_file: &Option<PathBuf>,
) -> io::Result<OutputLocation<Box<dyn Write>>> {
let output: OutputLocation<Box<dyn Write>> = match test_results_file {
Some(results_file_path) => {
let file_output =
OpenOptions::new().write(true).create_new(true).open(results_file_path)?;

OutputLocation::Raw(Box::new(file_output))
}
None => match term::stdout() {
None => OutputLocation::Raw(Box::new(io::stdout().lock())),
Some(t) => OutputLocation::Pretty(t),
},
};
Ok(output)
}

// Updates `ConsoleTestState` depending on result of the test execution.
fn handle_test_result(st: &mut ConsoleTestState, completed_test: CompletedTest) {
let test = completed_test.desc;
Expand Down Expand Up @@ -284,10 +299,7 @@ fn on_test_event(
/// A simple console test runner.
/// Runs provided tests reporting process and results to the stdout.
pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<bool> {
let output = match term::stdout() {
None => OutputLocation::Raw(io::stdout()),
Some(t) => OutputLocation::Pretty(t),
};
let output = build_test_output(&opts.test_results_file)?;

let max_name_len = tests
.iter()
Expand Down
4 changes: 4 additions & 0 deletions library/test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ mod types;
#[cfg(test)]
mod tests;

#[allow(dead_code)] // Not used in all configurations.
#[cfg(test)]
mod test_helpers;

use core::any::Any;

use event::{CompletedTest, TestEvent};
Expand Down
61 changes: 61 additions & 0 deletions library/test/src/test_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::hash::{BuildHasher, Hash, Hasher, RandomState};
use std::path::PathBuf;
use std::{env, fs, thread};

use rand::{RngCore, SeedableRng};

use crate::panic::Location;

/// Test-only replacement for `rand::thread_rng()`, which is unusable for
/// us, as we want to allow running stdlib tests on tier-3 targets which may
/// not have `getrandom` support.
///
/// Does a bit of a song and dance to ensure that the seed is different on
/// each call (as some tests sadly rely on this), but doesn't try that hard.
///
/// This is duplicated in the `core`, `alloc` test suites (as well as
/// `std`'s integration tests), but figuring out a mechanism to share these
/// seems far more painful than copy-pasting a 7 line function a couple
/// times, given that even under a perma-unstable feature, I don't think we
/// want to expose types from `rand` from `std`.
#[track_caller]
pub(crate) fn test_rng() -> rand_xorshift::XorShiftRng {
let mut hasher = RandomState::new().build_hasher();
Location::caller().hash(&mut hasher);
let hc64 = hasher.finish();
let seed_vec = hc64.to_le_bytes().into_iter().chain(0u8..8).collect::<Vec<u8>>();
let seed: [u8; 16] = seed_vec.as_slice().try_into().unwrap();
SeedableRng::from_seed(seed)
}

pub(crate) struct TempDir(PathBuf);

impl TempDir {
pub(crate) fn join(&self, path: &str) -> PathBuf {
let TempDir(ref p) = *self;
p.join(path)
}
}

impl Drop for TempDir {
fn drop(&mut self) {
// Gee, seeing how we're testing the fs module I sure hope that we
// at least implement this correctly!
let TempDir(ref p) = *self;
let result = fs::remove_dir_all(p);
// Avoid panicking while panicking as this causes the process to
// immediately abort, without displaying test results.
if !thread::panicking() {
result.unwrap();
}
}
}

#[track_caller] // for `test_rng`
pub(crate) fn tmpdir() -> TempDir {
let p = env::temp_dir();
let mut r = test_rng();
let ret = p.join(&format!("rust-{}", r.next_u32()));
fs::create_dir(&ret).unwrap();
TempDir(ret)
}
77 changes: 65 additions & 12 deletions library/test/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
use std::fs::{self, File};
use std::path::PathBuf;

use rand::RngCore;

use super::*;
use crate::{
console::OutputLocation,
formatters::PrettyFormatter,
test::{
MetricMap,
// FIXME (introduced by #65251)
// ShouldPanic, StaticTestName, TestDesc, TestDescAndFn, TestOpts, TestTimeOptions,
// TestType, TrFailedMsg, TrIgnored, TrOk,
parse_opts,
},
time::{TestTimeOptions, TimeThreshold},
};
use crate::console::{OutputLocation, build_test_output};
use crate::formatters::PrettyFormatter;
use crate::test::{MetricMap, parse_opts};
use crate::test_helpers::{test_rng, tmpdir};
use crate::time::{TestTimeOptions, TimeThreshold};

impl TestOpts {
fn new() -> TestOpts {
Expand All @@ -24,6 +22,7 @@ impl TestOpts {
run_tests: false,
bench_benchmarks: false,
logfile: None,
test_results_file: None,
nocapture: false,
color: AutoColor,
format: OutputFormat::Pretty,
Expand Down Expand Up @@ -468,6 +467,29 @@ fn parse_include_ignored_flag() {
assert_eq!(opts.run_ignored, RunIgnored::Yes);
}

#[test]
fn parse_test_results_file_flag_reads_value() {
let args = vec![
"progname".to_string(),
"filter".to_string(),
"--test-results-file".to_string(),
"expected_path_to_results_file".to_string(),
];
let opts = parse_opts(&args).unwrap().unwrap();
assert_eq!(opts.test_results_file, Some(PathBuf::from("expected_path_to_results_file")));
}

#[test]
fn parse_test_results_file_requires_value() {
let args =
vec!["progname".to_string(), "filter".to_string(), "--test-results-file".to_string()];
let maybe_opts = parse_opts(&args).unwrap();
assert_eq!(
maybe_opts.err(),
Some("Argument to option 'test-results-file' missing".to_string())
);
}

#[test]
fn filter_for_ignored_option() {
// When we run ignored tests the test filter should filter out all the
Expand Down Expand Up @@ -912,3 +934,34 @@ fn test_dyn_bench_returning_err_fails_when_run_as_test() {
let result = rx.recv().unwrap().result;
assert_eq!(result, TrFailed);
}

#[test]
fn test_result_output_propagated_to_file() {
let tmpdir = tmpdir();
let test_results_file = tmpdir.join("test_results");
let mut output = build_test_output(&Some(test_results_file.clone())).unwrap();
let random_str = format!("test_result_file_contents_{}", test_rng().next_u32());

match output {
OutputLocation::Raw(ref mut m) => {
m.write_all(random_str.as_bytes()).unwrap();
m.flush().unwrap()
}
OutputLocation::Pretty(_) => unreachable!(),
};

let file_contents = fs::read_to_string(test_results_file).unwrap();
assert_eq!(random_str, file_contents);
}

#[test]
fn test_result_output_bails_when_file_exists() {
let tmpdir = tmpdir();
let test_results_file = tmpdir.join("test_results");
let new_file = File::create(&test_results_file).unwrap();
drop(new_file);

let maybe_output = build_test_output(&Some(test_results_file));

assert_eq!(maybe_output.err().map(|e| e.kind()), Some(io::ErrorKind::AlreadyExists));
}
Loading