Skip to main content

test_r_core/output/
mod.rs

1mod ctrf;
2mod ipc;
3mod json;
4mod junit;
5mod pretty;
6mod term_progress;
7mod terse;
8
9use crate::args::{Arguments, FormatSetting};
10use crate::internal::{RegisteredTest, TestResult};
11use std::io::{Seek, SeekFrom, Write};
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::time::Duration;
15
16pub trait TestRunnerOutput: Send + Sync {
17    fn start_suite(&self, tests: &[RegisteredTest]);
18    fn start_running_test(&self, test: &RegisteredTest, idx: usize, count: usize);
19    fn repeat_running_test(
20        &self,
21        test: &RegisteredTest,
22        idx: usize,
23        count: usize,
24        attempt: usize,
25        max_attempts: usize,
26        reason: &str,
27    );
28    fn finished_running_test(
29        &self,
30        test: &RegisteredTest,
31        idx: usize,
32        count: usize,
33        result: &TestResult,
34    );
35    fn finished_suite(
36        &self,
37        registered_tests: &[RegisteredTest],
38        results: &[(RegisteredTest, TestResult)],
39        exec_time: Duration,
40    );
41    fn test_list(&self, registered_tests: &[RegisteredTest]);
42
43    fn warning(&self, message: &str) {
44        eprintln!("{message}");
45    }
46}
47
48pub(crate) fn write_failure_summary_to_stderr(
49    results: &[(RegisteredTest, TestResult)],
50    exec_time: Duration,
51) {
52    let failed: Vec<_> = results
53        .iter()
54        .filter(|(_, result)| result.is_failed())
55        .collect();
56    if failed.is_empty() {
57        return;
58    }
59    let passed = results
60        .iter()
61        .filter(|(_, result)| result.is_passed() || result.is_benchmarked())
62        .count();
63    let ignored = results
64        .iter()
65        .filter(|(_, result)| result.is_ignored())
66        .count();
67
68    eprintln!();
69    eprintln!(
70        "test result: FAILED; {} passed; {} failed; {} ignored; finished in {:.3}s",
71        passed,
72        failed.len(),
73        ignored,
74        exec_time.as_secs_f64()
75    );
76    eprintln!();
77    eprintln!("Failed tests:");
78    for (test, result) in &failed {
79        eprintln!(
80            " - {} ({})",
81            test.fully_qualified_name(),
82            result.failure_message().as_deref().unwrap_or("???"),
83        );
84    }
85    eprintln!();
86}
87
88pub fn test_runner_output(args: &Arguments) -> Arc<dyn TestRunnerOutput> {
89    if args.ipc.is_some() {
90        Arc::new(ipc::IpcWorkerOutput::new())
91    } else if args.quiet {
92        Arc::new(terse::Terse::new())
93    } else {
94        let logfile = args.logfile.as_ref().map(PathBuf::from);
95        match args.format.unwrap_or_default() {
96            FormatSetting::Pretty => Arc::new(pretty::Pretty::new(
97                args.color.unwrap_or_default(),
98                args.show_output,
99                logfile,
100                args.report_time,
101                args.unit_test_threshold(),
102                args.integration_test_threshold(),
103                args.show_stats,
104            )),
105            FormatSetting::Terse => Arc::new(terse::Terse::new()),
106            FormatSetting::Json => Arc::new(json::Json::new(args.show_output, logfile)),
107            FormatSetting::Junit => Arc::new(junit::JUnit::new(args.show_output, logfile)),
108            FormatSetting::Ctrf => Arc::new(ctrf::Ctrf::new(args.show_output, logfile)),
109        }
110    }
111}
112
113struct LogFile {
114    pub file: std::fs::File,
115}
116
117impl LogFile {
118    fn new(mut path: PathBuf, merged: bool) -> Self {
119        let cwd = std::env::current_dir().unwrap();
120        if path.is_relative() {
121            path = cwd.join(path);
122        }
123
124        if !path.parent().unwrap().exists() {
125            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
126        }
127
128        if !merged {
129            // Because of https://github.com/rust-lang/rust/issues/105424 we have to generate a unique log file name
130            // otherwise the core test runner will overwrite it
131            let uuid = uuid::Uuid::new_v4();
132            let stem = path
133                .file_stem()
134                .map(|s| s.to_string_lossy().to_string())
135                .unwrap_or_default();
136            let extension = path
137                .extension()
138                .map(|s| s.to_string_lossy().to_string())
139                .unwrap_or_default();
140            path.set_file_name(format!("{stem}-{uuid}.{extension}"));
141        }
142
143        eprintln!("Logging to {}", path.to_string_lossy());
144
145        let file = std::fs::OpenOptions::new()
146            .create(true)
147            .append(true)
148            .open(path.clone())
149            .unwrap_or_else(|_| panic!("Failed to open log file {}", path.to_string_lossy()));
150        LogFile { file }
151    }
152}
153
154enum StdoutOrLogFile {
155    Stdout(std::io::Stdout),
156    LogFile(LogFile),
157}
158
159impl StdoutOrLogFile {
160    fn reset_log_file(&mut self) -> std::io::Result<bool> {
161        if let StdoutOrLogFile::LogFile(logfile) = self {
162            logfile.file.set_len(0)?;
163            logfile.file.seek(SeekFrom::Start(0))?;
164            logfile.file.flush()?;
165            Ok(true)
166        } else {
167            Ok(false)
168        }
169    }
170}
171
172impl Write for StdoutOrLogFile {
173    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
174        match self {
175            StdoutOrLogFile::Stdout(stdout) => stdout.write(buf),
176            StdoutOrLogFile::LogFile(logfile) => logfile.file.write(buf),
177        }
178    }
179
180    fn flush(&mut self) -> std::io::Result<()> {
181        match self {
182            StdoutOrLogFile::Stdout(stdout) => stdout.flush(),
183            StdoutOrLogFile::LogFile(logfile) => logfile.file.flush(),
184        }
185    }
186}