diff --git a/library/Cargo.lock b/library/Cargo.lock index 5100b4d8176dc..92f16408fd387 100644 --- a/library/Cargo.lock +++ b/library/Cargo.lock @@ -378,6 +378,8 @@ dependencies = [ "core", "getopts", "libc", + "rand", + "rand_xorshift", "std", ] diff --git a/library/test/Cargo.toml b/library/test/Cargo.toml index 2a32a7dd76eed..7e037309123b7 100644 --- a/library/test/Cargo.toml +++ b/library/test/Cargo.toml @@ -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 } diff --git a/library/test/src/cli.rs b/library/test/src/cli.rs index 8840714a66238..80b78b255f3c9 100644 --- a/library/test/src/cli.rs +++ b/library/test/src/cli.rs @@ -18,6 +18,7 @@ pub struct TestOpts { pub run_tests: bool, pub bench_benchmarks: bool, pub logfile: Option, + pub test_results_file: Option, pub nocapture: bool, pub color: ColorConfig, pub format: OutputFormat, @@ -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") .optflag( "", "no-capture", @@ -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)?; @@ -298,6 +301,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes { run_tests, bench_benchmarks, logfile, + test_results_file, nocapture, color, format, @@ -500,3 +504,9 @@ fn get_log_file(matches: &getopts::Matches) -> OptPartRes> { Ok(logfile) } + +fn get_test_results_file(matches: &getopts::Matches) -> OptPartRes> { + let test_results_file = matches.opt_str("test-results-file").map(|s| PathBuf::from(&s)); + + Ok(test_results_file) +} diff --git a/library/test/src/console.rs b/library/test/src/console.rs index 8f29f1dada528..e0988bf6423a3 100644 --- a/library/test/src/console.rs +++ b/library/test/src/console.rs @@ -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; @@ -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) -> 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 = match opts.format { OutputFormat::Pretty | OutputFormat::Junit => { Box::new(PrettyFormatter::new(output, false, 0, false, None)) @@ -211,6 +208,24 @@ pub(crate) fn list_tests_console(opts: &TestOpts, tests: Vec) -> out.write_discovery_finish(&st) } +pub(crate) fn build_test_output( + test_results_file: &Option, +) -> io::Result>> { + let output: OutputLocation> = 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; @@ -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) -> io::Result { - 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() diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index 7f56d1e362698..cec6e190c76dc 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -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}; diff --git a/library/test/src/test_helpers.rs b/library/test/src/test_helpers.rs new file mode 100644 index 0000000000000..2b9f5f66475fb --- /dev/null +++ b/library/test/src/test_helpers.rs @@ -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::>(); + 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) +} diff --git a/library/test/src/tests.rs b/library/test/src/tests.rs index d986bd74f772b..dff15a3996b97 100644 --- a/library/test/src/tests.rs +++ b/library/test/src/tests.rs @@ -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 { @@ -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, @@ -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 @@ -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)); +}