mod ctrf;
mod ipc;
mod json;
mod junit;
mod pretty;
mod progress;
mod term_progress;
mod terse;
use crate::args::{Arguments, FormatSetting};
use crate::host_capture::{TerminalStderr, TerminalStdout};
use crate::internal::{RegisteredTest, TestResult};
use std::io::{Seek, SeekFrom, Write};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
pub trait TestRunnerOutput: Send + Sync {
fn start_suite(&self, tests: &[RegisteredTest]);
fn start_running_test(&self, test: &RegisteredTest, idx: usize, count: usize);
fn repeat_running_test(
&self,
test: &RegisteredTest,
idx: usize,
count: usize,
attempt: usize,
max_attempts: usize,
reason: &str,
);
fn finished_running_test(
&self,
test: &RegisteredTest,
idx: usize,
count: usize,
result: &TestResult,
);
fn finished_suite(
&self,
registered_tests: &[RegisteredTest],
results: &[(RegisteredTest, TestResult)],
exec_time: Duration,
);
fn test_list(&self, registered_tests: &[RegisteredTest]);
fn warning(&self, message: &str) {
let mut err = TerminalStderr;
let _ = writeln!(err, "{message}");
}
}
pub(crate) fn write_failure_summary_to_stderr(
results: &[(RegisteredTest, TestResult)],
exec_time: Duration,
) {
let failed: Vec<_> = results
.iter()
.filter(|(_, result)| result.is_failed())
.collect();
if failed.is_empty() {
return;
}
let passed = results
.iter()
.filter(|(_, result)| result.is_passed() || result.is_benchmarked())
.count();
let ignored = results
.iter()
.filter(|(_, result)| result.is_ignored())
.count();
let mut err = TerminalStderr;
let _ = writeln!(err);
let _ = writeln!(
err,
"test result: FAILED; {} passed; {} failed; {} ignored; finished in {:.3}s",
passed,
failed.len(),
ignored,
exec_time.as_secs_f64()
);
let _ = writeln!(err);
let _ = writeln!(err, "Failed tests:");
for (test, result) in &failed {
let _ = writeln!(
err,
" - {} ({})",
test.fully_qualified_name(),
result.failure_message().as_deref().unwrap_or("???"),
);
}
let _ = writeln!(err);
}
pub fn test_runner_output(args: &Arguments) -> Arc<dyn TestRunnerOutput> {
if args.ipc.is_some() {
Arc::new(ipc::IpcWorkerOutput::new())
} else if args.quiet {
Arc::new(terse::Terse::new())
} else {
let logfile = args.logfile.as_ref().map(PathBuf::from);
match args.format.unwrap_or_default() {
FormatSetting::Pretty => Arc::new(pretty::Pretty::new(
args.color.unwrap_or_default(),
args.show_output,
logfile,
args.report_time,
args.unit_test_threshold(),
args.integration_test_threshold(),
args.show_stats,
)),
FormatSetting::Terse => Arc::new(terse::Terse::new()),
FormatSetting::Json => Arc::new(json::Json::new(args.show_output, logfile)),
FormatSetting::Junit => Arc::new(junit::JUnit::new(args.show_output, logfile)),
FormatSetting::Ctrf => Arc::new(ctrf::Ctrf::new(args.show_output, logfile)),
}
}
}
struct LogFile {
pub file: std::fs::File,
}
impl LogFile {
fn new(mut path: PathBuf, merged: bool) -> Self {
let cwd = std::env::current_dir().unwrap();
if path.is_relative() {
path = cwd.join(path);
}
if !path.parent().unwrap().exists() {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
}
if !merged {
let uuid = uuid::Uuid::new_v4();
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
let extension = path
.extension()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
path.set_file_name(format!("{stem}-{uuid}.{extension}"));
}
let mut err = TerminalStderr;
let _ = writeln!(err, "Logging to {}", path.to_string_lossy());
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path.clone())
.unwrap_or_else(|_| panic!("Failed to open log file {}", path.to_string_lossy()));
LogFile { file }
}
}
enum StdoutOrLogFile {
Stdout(TerminalStdout),
LogFile(LogFile),
}
impl StdoutOrLogFile {
fn stdout() -> Self {
StdoutOrLogFile::Stdout(TerminalStdout)
}
fn reset_log_file(&mut self) -> std::io::Result<bool> {
if let StdoutOrLogFile::LogFile(logfile) = self {
logfile.file.set_len(0)?;
logfile.file.seek(SeekFrom::Start(0))?;
logfile.file.flush()?;
Ok(true)
} else {
Ok(false)
}
}
}
impl Write for StdoutOrLogFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
StdoutOrLogFile::Stdout(stdout) => stdout.write(buf),
StdoutOrLogFile::LogFile(logfile) => logfile.file.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
StdoutOrLogFile::Stdout(stdout) => stdout.flush(),
StdoutOrLogFile::LogFile(logfile) => logfile.file.flush(),
}
}
}