diff --git a/Cargo.lock b/Cargo.lock index 87833e4edbd78..6a467eba9882e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5515,6 +5515,7 @@ dependencies = [ "libc", "panic_abort", "panic_unwind", + "rand", "std", ] diff --git a/library/test/Cargo.toml b/library/test/Cargo.toml index 4c42e3bae563e..afbd882559fba 100644 --- a/library/test/Cargo.toml +++ b/library/test/Cargo.toml @@ -10,3 +10,6 @@ core = { path = "../core" } panic_unwind = { path = "../panic_unwind" } panic_abort = { path = "../panic_abort" } libc = { version = "0.2.150", default-features = false } + +[dev-dependencies] +rand = { version = "0.8.5" } diff --git a/library/test/src/console.rs b/library/test/src/console.rs index f3918ba333aa0..8eaf30a88a4a8 100644 --- a/library/test/src/console.rs +++ b/library/test/src/console.rs @@ -3,7 +3,9 @@ use std::fs::File; use std::io; use std::io::prelude::Write; +use std::path::PathBuf; use std::time::Instant; +use std::vec; use super::{ bench::fmt_bench_samples, @@ -19,6 +21,11 @@ use super::{ types::{NamePadding, TestDesc, TestDescAndFn}, }; +pub trait Output { + fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()>; + fn write_plain(&mut self, word: &str) -> io::Result<()>; +} + /// Generic wrapper over stdout. pub enum OutputLocation { Pretty(Box), @@ -29,48 +36,98 @@ impl Write for OutputLocation { fn write(&mut self, buf: &[u8]) -> io::Result { match *self { OutputLocation::Pretty(ref mut term) => term.write(buf), - OutputLocation::Raw(ref mut stdout) => stdout.write(buf), + OutputLocation::Raw(ref mut stdout_or_file) => stdout_or_file.write(buf), } } fn flush(&mut self) -> io::Result<()> { match *self { OutputLocation::Pretty(ref mut term) => term.flush(), - OutputLocation::Raw(ref mut stdout) => stdout.flush(), + OutputLocation::Raw(ref mut stdout_or_file) => stdout_or_file.flush(), } } } -pub struct ConsoleTestDiscoveryState { - pub log_out: Option, - pub tests: usize, - pub benchmarks: usize, - pub ignored: usize, +impl Output for OutputLocation { + fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { + match self { + OutputLocation::Pretty(ref mut term) => { + term.fg(color)?; + term.write_all(word.as_bytes())?; + term.reset()?; + } + OutputLocation::Raw(ref mut stdout) => { + stdout.write_all(word.as_bytes())?; + } + } + + self.flush() + } + + fn write_plain(&mut self, word: &str) -> io::Result<()> { + self.write_all(word.as_bytes())?; + self.flush() + } } -impl ConsoleTestDiscoveryState { - pub fn new(opts: &TestOpts) -> io::Result { - let log_out = match opts.logfile { - Some(ref path) => Some(File::create(path)?), - None => None, +struct OutputMultiplexer { + pub outputs: Vec>, +} + +impl OutputMultiplexer { + pub fn new(lock_stdout: bool, logfile: &Option) -> io::Result { + let mut outputs: Vec> = vec![]; + + if lock_stdout { + let output = match term::stdout() { + None => OutputLocation::Raw(io::stdout().lock()), + Some(t) => OutputLocation::Pretty(t), + }; + outputs.push(Box::new(output)) + } else { + let output = match term::stdout() { + None => OutputLocation::Raw(io::stdout()), + Some(t) => OutputLocation::Pretty(t), + }; + outputs.push(Box::new(output)) + } + + match logfile { + Some(ref path) => outputs.push(Box::new(OutputLocation::Raw(File::create(path)?))), + None => (), }; - Ok(ConsoleTestDiscoveryState { log_out, tests: 0, benchmarks: 0, ignored: 0 }) + Ok(Self { outputs }) } +} - pub fn write_log(&mut self, msg: F) -> io::Result<()> - where - S: AsRef, - F: FnOnce() -> S, - { - match self.log_out { - None => Ok(()), - Some(ref mut o) => { - let msg = msg(); - let msg = msg.as_ref(); - o.write_all(msg.as_bytes()) - } +impl Output for OutputMultiplexer { + fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { + for output in &mut self.outputs { + output.write_pretty(word, color)?; } + + Ok(()) + } + + fn write_plain(&mut self, word: &str) -> io::Result<()> { + for output in &mut self.outputs { + output.write_plain(word)?; + } + + Ok(()) + } +} + +pub struct ConsoleTestDiscoveryState { + pub tests: usize, + pub benchmarks: usize, + pub ignored: usize, +} + +impl ConsoleTestDiscoveryState { + pub fn new() -> io::Result { + Ok(ConsoleTestDiscoveryState { tests: 0, benchmarks: 0, ignored: 0 }) } } @@ -171,21 +228,18 @@ impl ConsoleTestState { // List the tests to console, and optionally to logfile. Filters are honored. pub 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 mut multiplexer = OutputMultiplexer::new(true, &opts.logfile)?; let mut out: Box = match opts.format { OutputFormat::Pretty | OutputFormat::Junit => { - Box::new(PrettyFormatter::new(output, false, 0, false, None)) + Box::new(PrettyFormatter::new(&mut multiplexer, false, 0, false, None)) } - OutputFormat::Terse => Box::new(TerseFormatter::new(output, false, 0, false)), - OutputFormat::Json => Box::new(JsonFormatter::new(output)), + OutputFormat::Terse => Box::new(TerseFormatter::new(&mut multiplexer, false, 0, false)), + OutputFormat::Json => Box::new(JsonFormatter::new(&mut multiplexer)), }; - let mut st = ConsoleTestDiscoveryState::new(opts)?; out.write_discovery_start()?; + + let mut st = ConsoleTestDiscoveryState::new()?; for test in filter_tests(opts, tests).into_iter() { use crate::TestFn::*; @@ -205,7 +259,6 @@ pub fn list_tests_console(opts: &TestOpts, tests: Vec) -> io::Res st.ignored += if desc.ignore { 1 } else { 0 }; out.write_test_discovered(&desc, fntype)?; - st.write_log(|| format!("{fntype} {}\n", desc.name))?; } out.write_discovery_finish(&st) @@ -284,7 +337,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() { + let mut output = match term::stdout() { None => OutputLocation::Raw(io::stdout()), Some(t) => OutputLocation::Pretty(t), }; @@ -299,17 +352,20 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec) -> io::Resu let mut out: Box = match opts.format { OutputFormat::Pretty => Box::new(PrettyFormatter::new( - output, + &mut output, opts.use_color(), max_name_len, is_multithreaded, opts.time_options, )), - OutputFormat::Terse => { - Box::new(TerseFormatter::new(output, opts.use_color(), max_name_len, is_multithreaded)) - } - OutputFormat::Json => Box::new(JsonFormatter::new(output)), - OutputFormat::Junit => Box::new(JunitFormatter::new(output)), + OutputFormat::Terse => Box::new(TerseFormatter::new( + &mut output, + opts.use_color(), + max_name_len, + is_multithreaded, + )), + OutputFormat::Json => Box::new(JsonFormatter::new(&mut output)), + OutputFormat::Junit => Box::new(JunitFormatter::new(&mut output)), }; let mut st = ConsoleTestState::new(opts)?; diff --git a/library/test/src/formatters/json.rs b/library/test/src/formatters/json.rs index 47c4e7757e40c..88a4e013d267a 100644 --- a/library/test/src/formatters/json.rs +++ b/library/test/src/formatters/json.rs @@ -1,19 +1,19 @@ -use std::{borrow::Cow, io, io::prelude::Write}; +use std::{borrow::Cow, io}; use super::OutputFormatter; use crate::{ - console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, + console::{ConsoleTestDiscoveryState, ConsoleTestState, Output}, test_result::TestResult, time, types::TestDesc, }; -pub(crate) struct JsonFormatter { - out: OutputLocation, +pub(crate) struct JsonFormatter<'a> { + out: &'a mut dyn Output, } -impl JsonFormatter { - pub fn new(out: OutputLocation) -> Self { +impl<'a> JsonFormatter<'a> { + pub fn new(out: &'a mut dyn Output) -> Self { Self { out } } @@ -23,7 +23,7 @@ impl JsonFormatter { // by issuing `write_all` calls line-by-line. assert_eq!(s.chars().last(), Some('\n')); - self.out.write_all(s.as_ref()) + self.out.write_plain(s) } fn write_event( @@ -56,7 +56,7 @@ impl JsonFormatter { } } -impl OutputFormatter for JsonFormatter { +impl OutputFormatter for JsonFormatter<'_> { fn write_discovery_start(&mut self) -> io::Result<()> { self.writeln_message(concat!(r#"{ "type": "suite", "event": "discovery" }"#, "\n")) } diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs index a211ebf1ded16..5058342e79e1a 100644 --- a/library/test/src/formatters/junit.rs +++ b/library/test/src/formatters/junit.rs @@ -1,28 +1,28 @@ -use std::io::{self, prelude::Write}; +use std::io; use std::time::Duration; use super::OutputFormatter; use crate::{ - console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, + console::{ConsoleTestDiscoveryState, ConsoleTestState, Output}, test_result::TestResult, time, types::{TestDesc, TestType}, }; -pub struct JunitFormatter { - out: OutputLocation, +pub struct JunitFormatter<'a> { + out: &'a mut dyn Output, results: Vec<(TestDesc, TestResult, Duration, Vec)>, } -impl JunitFormatter { - pub fn new(out: OutputLocation) -> Self { +impl<'a> JunitFormatter<'a> { + pub fn new(out: &'a mut dyn Output) -> 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()) + self.out.write_plain(s) } } @@ -38,7 +38,7 @@ fn str_to_cdata(s: &str) -> String { format!("", escaped_output) } -impl OutputFormatter for JunitFormatter { +impl OutputFormatter for JunitFormatter<'_> { fn write_discovery_start(&mut self) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::NotFound, "Not yet implemented!")) } @@ -179,7 +179,7 @@ impl OutputFormatter for JunitFormatter { self.write_message("")?; self.write_message("")?; - self.out.write_all(b"\n")?; + self.out.write_plain("\n")?; Ok(state.failed == 0) } diff --git a/library/test/src/formatters/pretty.rs b/library/test/src/formatters/pretty.rs index 22654a3400b44..9411d8d9523ae 100644 --- a/library/test/src/formatters/pretty.rs +++ b/library/test/src/formatters/pretty.rs @@ -1,17 +1,17 @@ -use std::{io, io::prelude::Write}; +use std::io; use super::OutputFormatter; use crate::{ bench::fmt_bench_samples, - console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, + console::{ConsoleTestDiscoveryState, ConsoleTestState, Output}, term, test_result::TestResult, time, types::TestDesc, }; -pub(crate) struct PrettyFormatter { - out: OutputLocation, +pub(crate) struct PrettyFormatter<'a> { + out: &'a mut dyn Output, use_color: bool, time_options: Option, @@ -21,9 +21,9 @@ pub(crate) struct PrettyFormatter { is_multithreaded: bool, } -impl PrettyFormatter { +impl<'a> PrettyFormatter<'a> { pub fn new( - out: OutputLocation, + out: &'a mut dyn Output, use_color: bool, max_name_len: usize, is_multithreaded: bool, @@ -32,11 +32,6 @@ impl PrettyFormatter { PrettyFormatter { out, use_color, max_name_len, is_multithreaded, time_options } } - #[cfg(test)] - pub fn output_location(&self) -> &OutputLocation { - &self.out - } - pub fn write_ok(&mut self) -> io::Result<()> { self.write_short_result("ok", term::color::GREEN) } @@ -69,29 +64,13 @@ impl PrettyFormatter { self.write_pretty(result, color) } - pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { - match self.out { - OutputLocation::Pretty(ref mut term) => { - if self.use_color { - term.fg(color)?; - } - term.write_all(word.as_bytes())?; - if self.use_color { - term.reset()?; - } - term.flush() - } - OutputLocation::Raw(ref mut stdout) => { - stdout.write_all(word.as_bytes())?; - stdout.flush() - } - } + fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { + if self.use_color { self.out.write_pretty(word, color) } else { self.out.write_plain(word) } } - pub fn write_plain>(&mut self, s: S) -> io::Result<()> { + fn write_plain>(&mut self, s: S) -> io::Result<()> { let s = s.as_ref(); - self.out.write_all(s.as_bytes())?; - self.out.flush() + self.out.write_plain(s) } fn write_time( @@ -180,7 +159,7 @@ impl PrettyFormatter { } } -impl OutputFormatter for PrettyFormatter { +impl OutputFormatter for PrettyFormatter<'_> { fn write_discovery_start(&mut self) -> io::Result<()> { Ok(()) } diff --git a/library/test/src/formatters/terse.rs b/library/test/src/formatters/terse.rs index 875c66e5fa32c..fa20641505f9e 100644 --- a/library/test/src/formatters/terse.rs +++ b/library/test/src/formatters/terse.rs @@ -1,9 +1,9 @@ -use std::{io, io::prelude::Write}; +use std::io; use super::OutputFormatter; use crate::{ bench::fmt_bench_samples, - console::{ConsoleTestDiscoveryState, ConsoleTestState, OutputLocation}, + console::{ConsoleTestDiscoveryState, ConsoleTestState, Output}, term, test_result::TestResult, time, @@ -15,8 +15,8 @@ use crate::{ // result chars leaves 12 chars for a progress count like " 11704/12853". const QUIET_MODE_MAX_COLUMN: usize = 88; -pub(crate) struct TerseFormatter { - out: OutputLocation, +pub(crate) struct TerseFormatter<'a> { + out: &'a mut dyn Output, use_color: bool, is_multithreaded: bool, /// Number of columns to fill when aligning names @@ -27,9 +27,9 @@ pub(crate) struct TerseFormatter { total_test_count: usize, } -impl TerseFormatter { +impl<'a> TerseFormatter<'a> { pub fn new( - out: OutputLocation, + out: &'a mut dyn Output, use_color: bool, max_name_len: usize, is_multithreaded: bool, @@ -98,29 +98,13 @@ impl TerseFormatter { Ok(()) } - pub fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { - match self.out { - OutputLocation::Pretty(ref mut term) => { - if self.use_color { - term.fg(color)?; - } - term.write_all(word.as_bytes())?; - if self.use_color { - term.reset()?; - } - term.flush() - } - OutputLocation::Raw(ref mut stdout) => { - stdout.write_all(word.as_bytes())?; - stdout.flush() - } - } + fn write_pretty(&mut self, word: &str, color: term::color::Color) -> io::Result<()> { + if self.use_color { self.out.write_pretty(word, color) } else { self.out.write_plain(word) } } - pub fn write_plain>(&mut self, s: S) -> io::Result<()> { + fn write_plain>(&mut self, s: S) -> io::Result<()> { let s = s.as_ref(); - self.out.write_all(s.as_bytes())?; - self.out.flush() + self.out.write_plain(s) } pub fn write_outputs(&mut self, state: &ConsoleTestState) -> io::Result<()> { @@ -187,7 +171,7 @@ impl TerseFormatter { } } -impl OutputFormatter for TerseFormatter { +impl OutputFormatter for TerseFormatter<'_> { fn write_discovery_start(&mut self) -> io::Result<()> { Ok(()) } diff --git a/library/test/src/tests.rs b/library/test/src/tests.rs index 43a906ad298d1..005cabd3c3885 100644 --- a/library/test/src/tests.rs +++ b/library/test/src/tests.rs @@ -1,6 +1,11 @@ +use rand::RngCore; +use std::fs; +use std::path::PathBuf; + use super::*; use crate::{ + console::list_tests_console, console::OutputLocation, formatters::PrettyFormatter, test::{ @@ -39,6 +44,36 @@ impl TestOpts { } } +// These implementations of TempDir and tmpdir are forked from rust/library/std/src/sys_common/io.rs. +struct TempDir(PathBuf); + +impl TempDir { + fn join(&self, path: &str) -> PathBuf { + let TempDir(ref p) = *self; + p.join(path) + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + 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(); + } + } +} + +fn tmpdir() -> TempDir { + let p = env::temp_dir(); + let mut r = rand::thread_rng(); + let ret = p.join(&format!("rust-{}", r.next_u32())); + fs::create_dir(&ret).unwrap(); + TempDir(ret) +} + fn one_ignored_one_unignored_test() -> Vec { vec![ TestDescAndFn { @@ -858,7 +893,9 @@ fn should_sort_failures_before_printing_them() { test_type: TestType::Unknown, }; - let mut out = PrettyFormatter::new(OutputLocation::Raw(Vec::new()), false, 10, false, None); + let mut output = Vec::new(); + let mut raw = OutputLocation::Raw(&mut output); + let mut out = PrettyFormatter::new(&mut raw, false, 10, false, None); let st = console::ConsoleTestState { log_out: None, @@ -878,11 +915,8 @@ fn should_sort_failures_before_printing_them() { }; out.write_failures(&st).unwrap(); - let s = match out.output_location() { - &OutputLocation::Raw(ref m) => String::from_utf8_lossy(&m[..]), - &OutputLocation::Pretty(_) => unreachable!(), - }; + let s = String::from_utf8_lossy(&output[..]); let apos = s.find("a").unwrap(); let bpos = s.find("b").unwrap(); assert!(apos < bpos); @@ -922,3 +956,82 @@ fn test_dyn_bench_returning_err_fails_when_run_as_test() { let result = rx.recv().unwrap().result; assert_eq!(result, TrFailed); } + +#[test] +fn test_discovery_logfile_format() { + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + ignore_message: None, + source_file: "", + start_line: 0, + start_col: 0, + end_line: 0, + end_col: 0, + should_panic: ShouldPanic::No, + compile_fail: false, + no_run: false, + test_type: TestType::Unknown, + }, + testfn: DynTestFn(Box::new(move || Ok(()))), + }; + + let tmpdir = tmpdir(); + let output_path = &tmpdir.join("output.txt"); + + let opts = TestOpts { + run_tests: true, + logfile: Some(output_path.clone()), + format: OutputFormat::Pretty, + list: true, + ..TestOpts::new() + }; + list_tests_console(&opts, vec![desc]).unwrap(); + + let contents = fs::read_to_string(output_path).expect("`--logfile` did not create file"); + + // Split output at line breaks to make the comparison platform-agnostic regarding newline style. + let contents_lines = contents.as_str().lines().collect::>(); + + assert_eq!(contents_lines, vec!["whatever: test", "", "1 test, 0 benchmarks"]); +} + +#[test] +fn test_logfile_format() { + let desc = TestDescAndFn { + desc: TestDesc { + name: StaticTestName("whatever"), + ignore: false, + ignore_message: None, + source_file: "", + start_line: 0, + start_col: 0, + end_line: 0, + end_col: 0, + should_panic: ShouldPanic::No, + compile_fail: false, + no_run: false, + test_type: TestType::Unknown, + }, + testfn: DynTestFn(Box::new(move || Ok(()))), + }; + + let tmpdir = tmpdir(); + let output_path = &tmpdir.join("output.txt"); + + let opts = TestOpts { + run_tests: true, + logfile: Some(output_path.clone()), + format: OutputFormat::Pretty, + ..TestOpts::new() + }; + run_tests_console(&opts, vec![desc]).unwrap(); + + let contents = fs::read_to_string(output_path).expect("`--logfile` did not create file"); + + // Split output at line breaks to make the comparison platform-agnostic regarding newline style. + let contents_lines = contents.as_str().lines().collect::>(); + + assert_eq!(contents_lines, vec!["ok whatever"]); +}