From 698f8c2f01ea549d77d7dc3338a12e04c11057b9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 14:02:58 +0200 Subject: Adding upstream version 1.64.0+dfsg1. Signed-off-by: Daniel Baumann --- library/test/src/formatters/json.rs | 260 +++++++++++++++++++++++++++++++ library/test/src/formatters/junit.rs | 180 ++++++++++++++++++++++ library/test/src/formatters/mod.rs | 42 +++++ library/test/src/formatters/pretty.rs | 281 ++++++++++++++++++++++++++++++++++ library/test/src/formatters/terse.rs | 259 +++++++++++++++++++++++++++++++ 5 files changed, 1022 insertions(+) create mode 100644 library/test/src/formatters/json.rs create mode 100644 library/test/src/formatters/junit.rs create mode 100644 library/test/src/formatters/mod.rs create mode 100644 library/test/src/formatters/pretty.rs create mode 100644 library/test/src/formatters/terse.rs (limited to 'library/test/src/formatters') diff --git a/library/test/src/formatters/json.rs b/library/test/src/formatters/json.rs new file mode 100644 index 000000000..c07fdafb1 --- /dev/null +++ b/library/test/src/formatters/json.rs @@ -0,0 +1,260 @@ +use std::{borrow::Cow, io, io::prelude::Write}; + +use super::OutputFormatter; +use crate::{ + console::{ConsoleTestState, OutputLocation}, + test_result::TestResult, + time, + types::TestDesc, +}; + +pub(crate) struct JsonFormatter { + out: OutputLocation, +} + +impl JsonFormatter { + pub fn new(out: OutputLocation) -> Self { + Self { out } + } + + fn writeln_message(&mut self, s: &str) -> io::Result<()> { + assert!(!s.contains('\n')); + + self.out.write_all(s.as_ref())?; + self.out.write_all(b"\n") + } + + fn write_message(&mut self, s: &str) -> io::Result<()> { + assert!(!s.contains('\n')); + + self.out.write_all(s.as_ref()) + } + + fn write_event( + &mut self, + ty: &str, + name: &str, + evt: &str, + exec_time: Option<&time::TestExecTime>, + stdout: Option>, + extra: Option<&str>, + ) -> io::Result<()> { + // A doc test's name includes a filename which must be escaped for correct json. + self.write_message(&*format!( + r#"{{ "type": "{}", "name": "{}", "event": "{}""#, + ty, + EscapedString(name), + evt + ))?; + if let Some(exec_time) = exec_time { + self.write_message(&*format!(r#", "exec_time": {}"#, exec_time.0.as_secs_f64()))?; + } + if let Some(stdout) = stdout { + self.write_message(&*format!(r#", "stdout": "{}""#, EscapedString(stdout)))?; + } + if let Some(extra) = extra { + self.write_message(&*format!(r#", {}"#, extra))?; + } + self.writeln_message(" }") + } +} + +impl OutputFormatter for JsonFormatter { + fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option) -> io::Result<()> { + let shuffle_seed_json = if let Some(shuffle_seed) = shuffle_seed { + format!(r#", "shuffle_seed": {}"#, shuffle_seed) + } else { + String::new() + }; + self.writeln_message(&*format!( + r#"{{ "type": "suite", "event": "started", "test_count": {}{} }}"#, + test_count, shuffle_seed_json + )) + } + + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { + self.writeln_message(&*format!( + r#"{{ "type": "test", "event": "started", "name": "{}" }}"#, + EscapedString(desc.name.as_slice()) + )) + } + + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + exec_time: Option<&time::TestExecTime>, + stdout: &[u8], + state: &ConsoleTestState, + ) -> io::Result<()> { + let display_stdout = state.options.display_output || *result != TestResult::TrOk; + let stdout = if display_stdout && !stdout.is_empty() { + Some(String::from_utf8_lossy(stdout)) + } else { + None + }; + match *result { + TestResult::TrOk => { + self.write_event("test", desc.name.as_slice(), "ok", exec_time, stdout, None) + } + + TestResult::TrFailed => { + self.write_event("test", desc.name.as_slice(), "failed", exec_time, stdout, None) + } + + TestResult::TrTimedFail => self.write_event( + "test", + desc.name.as_slice(), + "failed", + exec_time, + stdout, + Some(r#""reason": "time limit exceeded""#), + ), + + TestResult::TrFailedMsg(ref m) => self.write_event( + "test", + desc.name.as_slice(), + "failed", + exec_time, + stdout, + Some(&*format!(r#""message": "{}""#, EscapedString(m))), + ), + + TestResult::TrIgnored => self.write_event( + "test", + desc.name.as_slice(), + "ignored", + exec_time, + stdout, + desc.ignore_message + .map(|msg| format!(r#""message": "{}""#, EscapedString(msg))) + .as_deref(), + ), + + TestResult::TrBench(ref bs) => { + let median = bs.ns_iter_summ.median as usize; + let deviation = (bs.ns_iter_summ.max - bs.ns_iter_summ.min) as usize; + + let mbps = if bs.mb_s == 0 { + String::new() + } else { + format!(r#", "mib_per_second": {}"#, bs.mb_s) + }; + + let line = format!( + "{{ \"type\": \"bench\", \ + \"name\": \"{}\", \ + \"median\": {}, \ + \"deviation\": {}{} }}", + EscapedString(desc.name.as_slice()), + median, + deviation, + mbps + ); + + self.writeln_message(&*line) + } + } + } + + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { + self.writeln_message(&*format!( + r#"{{ "type": "test", "event": "timeout", "name": "{}" }}"#, + EscapedString(desc.name.as_slice()) + )) + } + + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + self.write_message(&*format!( + "{{ \"type\": \"suite\", \ + \"event\": \"{}\", \ + \"passed\": {}, \ + \"failed\": {}, \ + \"ignored\": {}, \ + \"measured\": {}, \ + \"filtered_out\": {}", + if state.failed == 0 { "ok" } else { "failed" }, + state.passed, + state.failed, + state.ignored, + state.measured, + state.filtered_out, + ))?; + + if let Some(ref exec_time) = state.exec_time { + let time_str = format!(", \"exec_time\": {}", exec_time.0.as_secs_f64()); + self.write_message(&time_str)?; + } + + self.writeln_message(" }")?; + + Ok(state.failed == 0) + } +} + +/// A formatting utility used to print strings with characters in need of escaping. +/// Base code taken form `libserialize::json::escape_str` +struct EscapedString>(S); + +impl> std::fmt::Display for EscapedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let mut start = 0; + + for (i, byte) in self.0.as_ref().bytes().enumerate() { + let escaped = match byte { + b'"' => "\\\"", + b'\\' => "\\\\", + b'\x00' => "\\u0000", + b'\x01' => "\\u0001", + b'\x02' => "\\u0002", + b'\x03' => "\\u0003", + b'\x04' => "\\u0004", + b'\x05' => "\\u0005", + b'\x06' => "\\u0006", + b'\x07' => "\\u0007", + b'\x08' => "\\b", + b'\t' => "\\t", + b'\n' => "\\n", + b'\x0b' => "\\u000b", + b'\x0c' => "\\f", + b'\r' => "\\r", + b'\x0e' => "\\u000e", + b'\x0f' => "\\u000f", + b'\x10' => "\\u0010", + b'\x11' => "\\u0011", + b'\x12' => "\\u0012", + b'\x13' => "\\u0013", + b'\x14' => "\\u0014", + b'\x15' => "\\u0015", + b'\x16' => "\\u0016", + b'\x17' => "\\u0017", + b'\x18' => "\\u0018", + b'\x19' => "\\u0019", + b'\x1a' => "\\u001a", + b'\x1b' => "\\u001b", + b'\x1c' => "\\u001c", + b'\x1d' => "\\u001d", + b'\x1e' => "\\u001e", + b'\x1f' => "\\u001f", + b'\x7f' => "\\u007f", + _ => { + continue; + } + }; + + if start < i { + f.write_str(&self.0.as_ref()[start..i])?; + } + + f.write_str(escaped)?; + + start = i + 1; + } + + if start != self.0.as_ref().len() { + f.write_str(&self.0.as_ref()[start..])?; + } + + Ok(()) + } +} diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs new file mode 100644 index 000000000..e6fb4f570 --- /dev/null +++ b/library/test/src/formatters/junit.rs @@ -0,0 +1,180 @@ +use std::io::{self, prelude::Write}; +use std::time::Duration; + +use super::OutputFormatter; +use crate::{ + console::{ConsoleTestState, OutputLocation}, + test_result::TestResult, + time, + types::{TestDesc, TestType}, +}; + +pub struct JunitFormatter { + out: OutputLocation, + results: Vec<(TestDesc, TestResult, Duration)>, +} + +impl JunitFormatter { + pub fn new(out: OutputLocation) -> 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()) + } +} + +impl OutputFormatter for JunitFormatter { + fn write_run_start( + &mut self, + _test_count: usize, + _shuffle_seed: Option, + ) -> io::Result<()> { + // We write xml header on run start + self.write_message("") + } + + fn write_test_start(&mut self, _desc: &TestDesc) -> io::Result<()> { + // We do not output anything on test start. + Ok(()) + } + + fn write_timeout(&mut self, _desc: &TestDesc) -> io::Result<()> { + // We do not output anything on test timeout. + Ok(()) + } + + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + exec_time: Option<&time::TestExecTime>, + _stdout: &[u8], + _state: &ConsoleTestState, + ) -> io::Result<()> { + // Because the testsuite node holds some of the information as attributes, we can't write it + // until all of the tests have finished. Instead of writing every result as they come in, we add + // them to a Vec and write them all at once when run is complete. + let duration = exec_time.map(|t| t.0).unwrap_or_default(); + self.results.push((desc.clone(), result.clone(), duration)); + Ok(()) + } + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + self.write_message("")?; + + self.write_message(&*format!( + "", + state.failed, state.total, state.ignored + ))?; + for (desc, result, duration) in std::mem::replace(&mut self.results, Vec::new()) { + let (class_name, test_name) = parse_class_name(&desc); + match result { + TestResult::TrIgnored => { /* no-op */ } + TestResult::TrFailed => { + self.write_message(&*format!( + "", + class_name, + test_name, + duration.as_secs_f64() + ))?; + self.write_message("")?; + self.write_message("")?; + } + + TestResult::TrFailedMsg(ref m) => { + self.write_message(&*format!( + "", + class_name, + test_name, + duration.as_secs_f64() + ))?; + self.write_message(&*format!(""))?; + self.write_message("")?; + } + + TestResult::TrTimedFail => { + self.write_message(&*format!( + "", + class_name, + test_name, + duration.as_secs_f64() + ))?; + self.write_message("")?; + self.write_message("")?; + } + + TestResult::TrBench(ref b) => { + self.write_message(&*format!( + "", + class_name, test_name, b.ns_iter_summ.sum + ))?; + } + + TestResult::TrOk => { + self.write_message(&*format!( + "", + class_name, + test_name, + duration.as_secs_f64() + ))?; + } + } + } + self.write_message("")?; + self.write_message("")?; + self.write_message("")?; + self.write_message("")?; + + self.out.write_all(b"\n")?; + + Ok(state.failed == 0) + } +} + +fn parse_class_name(desc: &TestDesc) -> (String, String) { + match desc.test_type { + TestType::UnitTest => parse_class_name_unit(desc), + TestType::DocTest => parse_class_name_doc(desc), + TestType::IntegrationTest => parse_class_name_integration(desc), + TestType::Unknown => (String::from("unknown"), String::from(desc.name.as_slice())), + } +} + +fn parse_class_name_unit(desc: &TestDesc) -> (String, String) { + // Module path => classname + // Function name => name + let module_segments: Vec<&str> = desc.name.as_slice().split("::").collect(); + let (class_name, test_name) = match module_segments[..] { + [test] => (String::from("crate"), String::from(test)), + [ref path @ .., test] => (path.join("::"), String::from(test)), + [..] => unreachable!(), + }; + (class_name, test_name) +} + +fn parse_class_name_doc(desc: &TestDesc) -> (String, String) { + // File path => classname + // Line # => test name + let segments: Vec<&str> = desc.name.as_slice().split(" - ").collect(); + let (class_name, test_name) = match segments[..] { + [file, line] => (String::from(file.trim()), String::from(line.trim())), + [..] => unreachable!(), + }; + (class_name, test_name) +} + +fn parse_class_name_integration(desc: &TestDesc) -> (String, String) { + (String::from("integration"), String::from(desc.name.as_slice())) +} diff --git a/library/test/src/formatters/mod.rs b/library/test/src/formatters/mod.rs new file mode 100644 index 000000000..cb8085975 --- /dev/null +++ b/library/test/src/formatters/mod.rs @@ -0,0 +1,42 @@ +use std::{io, io::prelude::Write}; + +use crate::{ + console::ConsoleTestState, + test_result::TestResult, + time, + types::{TestDesc, TestName}, +}; + +mod json; +mod junit; +mod pretty; +mod terse; + +pub(crate) use self::json::JsonFormatter; +pub(crate) use self::junit::JunitFormatter; +pub(crate) use self::pretty::PrettyFormatter; +pub(crate) use self::terse::TerseFormatter; + +pub(crate) trait OutputFormatter { + fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option) -> io::Result<()>; + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()>; + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()>; + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + exec_time: Option<&time::TestExecTime>, + stdout: &[u8], + state: &ConsoleTestState, + ) -> io::Result<()>; + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result; +} + +pub(crate) fn write_stderr_delimiter(test_output: &mut Vec, test_name: &TestName) { + match test_output.last() { + Some(b'\n') => (), + Some(_) => test_output.push(b'\n'), + None => (), + } + writeln!(test_output, "---- {} stderr ----", test_name).unwrap(); +} diff --git a/library/test/src/formatters/pretty.rs b/library/test/src/formatters/pretty.rs new file mode 100644 index 000000000..694202229 --- /dev/null +++ b/library/test/src/formatters/pretty.rs @@ -0,0 +1,281 @@ +use std::{io, io::prelude::Write}; + +use super::OutputFormatter; +use crate::{ + bench::fmt_bench_samples, + console::{ConsoleTestState, OutputLocation}, + term, + test_result::TestResult, + time, + types::TestDesc, +}; + +pub(crate) struct PrettyFormatter { + out: OutputLocation, + use_color: bool, + time_options: Option, + + /// Number of columns to fill when aligning names + max_name_len: usize, + + is_multithreaded: bool, +} + +impl PrettyFormatter { + pub fn new( + out: OutputLocation, + use_color: bool, + max_name_len: usize, + is_multithreaded: bool, + time_options: Option, + ) -> Self { + 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) + } + + pub fn write_failed(&mut self) -> io::Result<()> { + self.write_short_result("FAILED", term::color::RED) + } + + pub fn write_ignored(&mut self, message: Option<&'static str>) -> io::Result<()> { + if let Some(message) = message { + self.write_short_result(&format!("ignored, {}", message), term::color::YELLOW) + } else { + self.write_short_result("ignored", term::color::YELLOW) + } + } + + pub fn write_time_failed(&mut self) -> io::Result<()> { + self.write_short_result("FAILED (time limit exceeded)", term::color::RED) + } + + pub fn write_bench(&mut self) -> io::Result<()> { + self.write_pretty("bench", term::color::CYAN) + } + + pub fn write_short_result( + &mut self, + result: &str, + color: term::color::Color, + ) -> io::Result<()> { + 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() + } + } + } + + pub fn write_plain>(&mut self, s: S) -> io::Result<()> { + let s = s.as_ref(); + self.out.write_all(s.as_bytes())?; + self.out.flush() + } + + fn write_time( + &mut self, + desc: &TestDesc, + exec_time: Option<&time::TestExecTime>, + ) -> io::Result<()> { + if let (Some(opts), Some(time)) = (self.time_options, exec_time) { + let time_str = format!(" <{time}>"); + + let color = if self.use_color { + if opts.is_critical(desc, time) { + Some(term::color::RED) + } else if opts.is_warn(desc, time) { + Some(term::color::YELLOW) + } else { + None + } + } else { + None + }; + + match color { + Some(color) => self.write_pretty(&time_str, color)?, + None => self.write_plain(&time_str)?, + } + } + + Ok(()) + } + + fn write_results( + &mut self, + inputs: &Vec<(TestDesc, Vec)>, + results_type: &str, + ) -> io::Result<()> { + let results_out_str = format!("\n{results_type}:\n"); + + self.write_plain(&results_out_str)?; + + let mut results = Vec::new(); + let mut stdouts = String::new(); + for &(ref f, ref stdout) in inputs { + results.push(f.name.to_string()); + if !stdout.is_empty() { + stdouts.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + stdouts.push_str(&output); + stdouts.push('\n'); + } + } + if !stdouts.is_empty() { + self.write_plain("\n")?; + self.write_plain(&stdouts)?; + } + + self.write_plain(&results_out_str)?; + results.sort(); + for name in &results { + self.write_plain(&format!(" {name}\n"))?; + } + Ok(()) + } + + pub fn write_successes(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_results(&state.not_failures, "successes") + } + + pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_results(&state.failures, "failures") + } + + pub fn write_time_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_results(&state.time_failures, "failures (time limit exceeded)") + } + + fn write_test_name(&mut self, desc: &TestDesc) -> io::Result<()> { + let name = desc.padded_name(self.max_name_len, desc.name.padding()); + if let Some(test_mode) = desc.test_mode() { + self.write_plain(&format!("test {name} - {test_mode} ... "))?; + } else { + self.write_plain(&format!("test {name} ... "))?; + } + + Ok(()) + } +} + +impl OutputFormatter for PrettyFormatter { + fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option) -> io::Result<()> { + let noun = if test_count != 1 { "tests" } else { "test" }; + let shuffle_seed_msg = if let Some(shuffle_seed) = shuffle_seed { + format!(" (shuffle seed: {shuffle_seed})") + } else { + String::new() + }; + self.write_plain(&format!("\nrunning {test_count} {noun}{shuffle_seed_msg}\n")) + } + + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { + // When running tests concurrently, we should not print + // the test's name as the result will be mis-aligned. + // When running the tests serially, we print the name here so + // that the user can see which test hangs. + if !self.is_multithreaded { + self.write_test_name(desc)?; + } + + Ok(()) + } + + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + exec_time: Option<&time::TestExecTime>, + _: &[u8], + _: &ConsoleTestState, + ) -> io::Result<()> { + if self.is_multithreaded { + self.write_test_name(desc)?; + } + + match *result { + TestResult::TrOk => self.write_ok()?, + TestResult::TrFailed | TestResult::TrFailedMsg(_) => self.write_failed()?, + TestResult::TrIgnored => self.write_ignored(desc.ignore_message)?, + TestResult::TrBench(ref bs) => { + self.write_bench()?; + self.write_plain(&format!(": {}", fmt_bench_samples(bs)))?; + } + TestResult::TrTimedFail => self.write_time_failed()?, + } + + self.write_time(desc, exec_time)?; + self.write_plain("\n") + } + + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { + self.write_plain(&format!( + "test {} has been running for over {} seconds\n", + desc.name, + time::TEST_WARN_TIMEOUT_S + )) + } + + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + if state.options.display_output { + self.write_successes(state)?; + } + let success = state.failed == 0; + if !success { + if !state.failures.is_empty() { + self.write_failures(state)?; + } + + if !state.time_failures.is_empty() { + self.write_time_failures(state)?; + } + } + + self.write_plain("\ntest result: ")?; + + if success { + // There's no parallelism at this point so it's safe to use color + self.write_pretty("ok", term::color::GREEN)?; + } else { + self.write_pretty("FAILED", term::color::RED)?; + } + + let s = format!( + ". {} passed; {} failed; {} ignored; {} measured; {} filtered out", + state.passed, state.failed, state.ignored, state.measured, state.filtered_out + ); + + self.write_plain(&s)?; + + if let Some(ref exec_time) = state.exec_time { + let time_str = format!("; finished in {exec_time}"); + self.write_plain(&time_str)?; + } + + self.write_plain("\n\n")?; + + Ok(success) + } +} diff --git a/library/test/src/formatters/terse.rs b/library/test/src/formatters/terse.rs new file mode 100644 index 000000000..5dace8bae --- /dev/null +++ b/library/test/src/formatters/terse.rs @@ -0,0 +1,259 @@ +use std::{io, io::prelude::Write}; + +use super::OutputFormatter; +use crate::{ + bench::fmt_bench_samples, + console::{ConsoleTestState, OutputLocation}, + term, + test_result::TestResult, + time, + types::NamePadding, + types::TestDesc, +}; + +// We insert a '\n' when the output hits 100 columns in quiet mode. 88 test +// 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, + use_color: bool, + is_multithreaded: bool, + /// Number of columns to fill when aligning names + max_name_len: usize, + + test_count: usize, + total_test_count: usize, +} + +impl TerseFormatter { + pub fn new( + out: OutputLocation, + use_color: bool, + max_name_len: usize, + is_multithreaded: bool, + ) -> Self { + TerseFormatter { + out, + use_color, + max_name_len, + is_multithreaded, + test_count: 0, + total_test_count: 0, // initialized later, when write_run_start is called + } + } + + pub fn write_ok(&mut self) -> io::Result<()> { + self.write_short_result(".", term::color::GREEN) + } + + pub fn write_failed(&mut self) -> io::Result<()> { + self.write_short_result("F", term::color::RED) + } + + pub fn write_ignored(&mut self) -> io::Result<()> { + self.write_short_result("i", term::color::YELLOW) + } + + pub fn write_bench(&mut self) -> io::Result<()> { + self.write_pretty("bench", term::color::CYAN) + } + + pub fn write_short_result( + &mut self, + result: &str, + color: term::color::Color, + ) -> io::Result<()> { + self.write_pretty(result, color)?; + if self.test_count % QUIET_MODE_MAX_COLUMN == QUIET_MODE_MAX_COLUMN - 1 { + // We insert a new line regularly in order to flush the + // screen when dealing with line-buffered output (e.g., piping to + // `stamp` in the rust CI). + let out = format!(" {}/{}\n", self.test_count + 1, self.total_test_count); + self.write_plain(&out)?; + } + + self.test_count += 1; + 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() + } + } + } + + pub fn write_plain>(&mut self, s: S) -> io::Result<()> { + let s = s.as_ref(); + self.out.write_all(s.as_bytes())?; + self.out.flush() + } + + pub fn write_outputs(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_plain("\nsuccesses:\n")?; + let mut successes = Vec::new(); + let mut stdouts = String::new(); + for &(ref f, ref stdout) in &state.not_failures { + successes.push(f.name.to_string()); + if !stdout.is_empty() { + stdouts.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + stdouts.push_str(&output); + stdouts.push('\n'); + } + } + if !stdouts.is_empty() { + self.write_plain("\n")?; + self.write_plain(&stdouts)?; + } + + self.write_plain("\nsuccesses:\n")?; + successes.sort(); + for name in &successes { + self.write_plain(&format!(" {name}\n"))?; + } + Ok(()) + } + + pub fn write_failures(&mut self, state: &ConsoleTestState) -> io::Result<()> { + self.write_plain("\nfailures:\n")?; + let mut failures = Vec::new(); + let mut fail_out = String::new(); + for &(ref f, ref stdout) in &state.failures { + failures.push(f.name.to_string()); + if !stdout.is_empty() { + fail_out.push_str(&format!("---- {} stdout ----\n", f.name)); + let output = String::from_utf8_lossy(stdout); + fail_out.push_str(&output); + fail_out.push('\n'); + } + } + if !fail_out.is_empty() { + self.write_plain("\n")?; + self.write_plain(&fail_out)?; + } + + self.write_plain("\nfailures:\n")?; + failures.sort(); + for name in &failures { + self.write_plain(&format!(" {name}\n"))?; + } + Ok(()) + } + + fn write_test_name(&mut self, desc: &TestDesc) -> io::Result<()> { + let name = desc.padded_name(self.max_name_len, desc.name.padding()); + if let Some(test_mode) = desc.test_mode() { + self.write_plain(&format!("test {name} - {test_mode} ... "))?; + } else { + self.write_plain(&format!("test {name} ... "))?; + } + + Ok(()) + } +} + +impl OutputFormatter for TerseFormatter { + fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option) -> io::Result<()> { + self.total_test_count = test_count; + let noun = if test_count != 1 { "tests" } else { "test" }; + let shuffle_seed_msg = if let Some(shuffle_seed) = shuffle_seed { + format!(" (shuffle seed: {shuffle_seed})") + } else { + String::new() + }; + self.write_plain(&format!("\nrunning {test_count} {noun}{shuffle_seed_msg}\n")) + } + + fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> { + // Remnants from old libtest code that used the padding value + // in order to indicate benchmarks. + // When running benchmarks, terse-mode should still print their name as if + // it is the Pretty formatter. + if !self.is_multithreaded && desc.name.padding() == NamePadding::PadOnRight { + self.write_test_name(desc)?; + } + + Ok(()) + } + + fn write_result( + &mut self, + desc: &TestDesc, + result: &TestResult, + _: Option<&time::TestExecTime>, + _: &[u8], + _: &ConsoleTestState, + ) -> io::Result<()> { + match *result { + TestResult::TrOk => self.write_ok(), + TestResult::TrFailed | TestResult::TrFailedMsg(_) | TestResult::TrTimedFail => { + self.write_failed() + } + TestResult::TrIgnored => self.write_ignored(), + TestResult::TrBench(ref bs) => { + if self.is_multithreaded { + self.write_test_name(desc)?; + } + self.write_bench()?; + self.write_plain(&format!(": {}\n", fmt_bench_samples(bs))) + } + } + } + + fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> { + self.write_plain(&format!( + "test {} has been running for over {} seconds\n", + desc.name, + time::TEST_WARN_TIMEOUT_S + )) + } + + fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result { + if state.options.display_output { + self.write_outputs(state)?; + } + let success = state.failed == 0; + if !success { + self.write_failures(state)?; + } + + self.write_plain("\ntest result: ")?; + + if success { + // There's no parallelism at this point so it's safe to use color + self.write_pretty("ok", term::color::GREEN)?; + } else { + self.write_pretty("FAILED", term::color::RED)?; + } + + let s = format!( + ". {} passed; {} failed; {} ignored; {} measured; {} filtered out", + state.passed, state.failed, state.ignored, state.measured, state.filtered_out + ); + + self.write_plain(&s)?; + + if let Some(ref exec_time) = state.exec_time { + let time_str = format!("; finished in {exec_time}"); + self.write_plain(&time_str)?; + } + + self.write_plain("\n\n")?; + + Ok(success) + } +} -- cgit v1.2.3