1//! Module providing interface for running tests in the console.
2
3use std::fs::File;
4use std::io;
5use std::io::prelude::Write;
6use std::time::Instant;
7
8use super::{
9 bench::fmt_bench_samples,
10 cli::TestOpts,
11 event::{CompletedTest, TestEvent},
12 filter_tests,
13 formatters::{JsonFormatter, OutputFormatter, PrettyFormatter, TerseFormatter},
14 helpers::{concurrency::get_concurrency, metrics::MetricMap},
15 options::{Options, OutputFormat},
16 run_tests,
17 test_result::TestResult,
18 time::{TestExecTime, TestSuiteExecTime},
19 types::{NamePadding, TestDesc, TestDescAndFn},
20};
21
22/// Generic wrapper over stdout.
23pub enum OutputLocation<T> {
24 Pretty(Box<term::StdoutTerminal>),
25 Raw(T),
26}
27
28impl<T: Write> Write for OutputLocation<T> {
29 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
30 match *self {
31 OutputLocation::Pretty(ref mut term: &mut Box + Send>) => term.write(buf),
32 OutputLocation::Raw(ref mut stdout: &mut T) => stdout.write(buf),
33 }
34 }
35
36 fn flush(&mut self) -> io::Result<()> {
37 match *self {
38 OutputLocation::Pretty(ref mut term: &mut Box + Send>) => term.flush(),
39 OutputLocation::Raw(ref mut stdout: &mut T) => stdout.flush(),
40 }
41 }
42}
43
44pub struct ConsoleTestState {
45 pub log_out: Option<File>,
46 pub total: usize,
47 pub passed: usize,
48 pub failed: usize,
49 pub ignored: usize,
50 pub allowed_fail: usize,
51 pub filtered_out: usize,
52 pub measured: usize,
53 pub exec_time: Option<TestSuiteExecTime>,
54 pub metrics: MetricMap,
55 pub failures: Vec<(TestDesc, Vec<u8>)>,
56 pub not_failures: Vec<(TestDesc, Vec<u8>)>,
57 pub time_failures: Vec<(TestDesc, Vec<u8>)>,
58 pub options: Options,
59}
60
61impl ConsoleTestState {
62 pub fn new(opts: &TestOpts) -> io::Result<ConsoleTestState> {
63 let log_out = match opts.logfile {
64 Some(ref path) => Some(File::create(path)?),
65 None => None,
66 };
67
68 Ok(ConsoleTestState {
69 log_out,
70 total: 0,
71 passed: 0,
72 failed: 0,
73 ignored: 0,
74 allowed_fail: 0,
75 filtered_out: 0,
76 measured: 0,
77 exec_time: None,
78 metrics: MetricMap::new(),
79 failures: Vec::new(),
80 not_failures: Vec::new(),
81 time_failures: Vec::new(),
82 options: opts.options,
83 })
84 }
85
86 pub fn write_log<F, S>(&mut self, msg: F) -> io::Result<()>
87 where
88 S: AsRef<str>,
89 F: FnOnce() -> S,
90 {
91 match self.log_out {
92 None => Ok(()),
93 Some(ref mut o) => {
94 let msg = msg();
95 let msg = msg.as_ref();
96 o.write_all(msg.as_bytes())
97 }
98 }
99 }
100
101 pub fn write_log_result(
102 &mut self,
103 test: &TestDesc,
104 result: &TestResult,
105 exec_time: Option<&TestExecTime>,
106 ) -> io::Result<()> {
107 self.write_log(|| {
108 format!(
109 "{} {}",
110 match *result {
111 TestResult::TrOk => "ok".to_owned(),
112 TestResult::TrFailed => "failed".to_owned(),
113 TestResult::TrFailedMsg(ref msg) => format!("failed: {}", msg),
114 TestResult::TrIgnored => "ignored".to_owned(),
115 TestResult::TrAllowedFail => "failed (allowed)".to_owned(),
116 TestResult::TrBench(ref bs) => fmt_bench_samples(bs),
117 TestResult::TrTimedFail => "failed (time limit exceeded)".to_owned(),
118 },
119 test.name,
120 )
121 })?;
122 if let Some(exec_time) = exec_time {
123 self.write_log(|| format!(" <{}>", exec_time))?;
124 }
125 self.write_log(|| "\n")
126 }
127
128 fn current_test_count(&self) -> usize {
129 self.passed + self.failed + self.ignored + self.measured + self.allowed_fail
130 }
131}
132
133// List the tests to console, and optionally to logfile. Filters are honored.
134pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<()> {
135 let mut output = match term::stdout() {
136 None => OutputLocation::Raw(io::stdout()),
137 Some(t) => OutputLocation::Pretty(t),
138 };
139
140 let quiet = opts.format == OutputFormat::Terse;
141 let mut st = ConsoleTestState::new(opts)?;
142
143 let mut ntest = 0;
144 let mut nbench = 0;
145
146 for test in filter_tests(&opts, tests) {
147 use crate::TestFn::*;
148
149 let TestDescAndFn { desc: TestDesc { name, .. }, testfn } = test;
150
151 let fntype = match testfn {
152 StaticTestFn(..) | DynTestFn(..) => {
153 ntest += 1;
154 "test"
155 }
156 StaticBenchFn(..) | DynBenchFn(..) => {
157 nbench += 1;
158 "benchmark"
159 }
160 };
161
162 writeln!(output, "{}: {}", name, fntype)?;
163 st.write_log(|| format!("{} {}\n", fntype, name))?;
164 }
165
166 fn plural(count: u32, s: &str) -> String {
167 match count {
168 1 => format!("{} {}", 1, s),
169 n => format!("{} {}s", n, s),
170 }
171 }
172
173 if !quiet {
174 if ntest != 0 || nbench != 0 {
175 writeln!(output)?;
176 }
177
178 writeln!(output, "{}, {}", plural(ntest, "test"), plural(nbench, "benchmark"))?;
179 }
180
181 Ok(())
182}
183
184// Updates `ConsoleTestState` depending on result of the test execution.
185fn handle_test_result(st: &mut ConsoleTestState, completed_test: CompletedTest) {
186 let test = completed_test.desc;
187 let stdout = completed_test.stdout;
188 match completed_test.result {
189 TestResult::TrOk => {
190 st.passed += 1;
191 st.not_failures.push((test, stdout));
192 }
193 TestResult::TrIgnored => st.ignored += 1,
194 TestResult::TrAllowedFail => st.allowed_fail += 1,
195 TestResult::TrBench(bs) => {
196 st.metrics.insert_metric(
197 test.name.as_slice(),
198 bs.ns_iter_summ.median,
199 bs.ns_iter_summ.max - bs.ns_iter_summ.min,
200 );
201 st.measured += 1
202 }
203 TestResult::TrFailed => {
204 st.failed += 1;
205 st.failures.push((test, stdout));
206 }
207 TestResult::TrFailedMsg(msg) => {
208 st.failed += 1;
209 let mut stdout = stdout;
210 stdout.extend_from_slice(format!("note: {}", msg).as_bytes());
211 st.failures.push((test, stdout));
212 }
213 TestResult::TrTimedFail => {
214 st.failed += 1;
215 st.time_failures.push((test, stdout));
216 }
217 }
218}
219
220// Handler for events that occur during test execution.
221// It is provided as a callback to the `run_tests` function.
222fn on_test_event(
223 event: &TestEvent,
224 st: &mut ConsoleTestState,
225 out: &mut dyn OutputFormatter,
226) -> io::Result<()> {
227 match (*event).clone() {
228 TestEvent::TeFiltered(ref filtered_tests) => {
229 st.total = filtered_tests.len();
230 out.write_run_start(filtered_tests.len())?;
231 }
232 TestEvent::TeFilteredOut(filtered_out) => {
233 st.filtered_out = filtered_out;
234 }
235 TestEvent::TeWait(ref test) => out.write_test_start(test)?,
236 TestEvent::TeTimeout(ref test) => out.write_timeout(test)?,
237 TestEvent::TeResult(completed_test) => {
238 let test = &completed_test.desc;
239 let result = &completed_test.result;
240 let exec_time = &completed_test.exec_time;
241 let stdout = &completed_test.stdout;
242
243 st.write_log_result(test, result, exec_time.as_ref())?;
244 out.write_result(test, result, exec_time.as_ref(), &*stdout, st)?;
245 handle_test_result(st, completed_test);
246 }
247 }
248
249 Ok(())
250}
251
252/// A simple console test runner.
253/// Runs provided tests reporting process and results to the stdout.
254pub fn run_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Result<bool> {
255 let output = match term::stdout() {
256 None => OutputLocation::Raw(io::stdout()),
257 Some(t) => OutputLocation::Pretty(t),
258 };
259
260 let max_name_len = tests
261 .iter()
262 .max_by_key(|t| len_if_padded(*t))
263 .map(|t| t.desc.name.as_slice().len())
264 .unwrap_or(0);
265
266 let is_multithreaded = opts.test_threads.unwrap_or_else(get_concurrency) > 1;
267
268 let mut out: Box<dyn OutputFormatter> = match opts.format {
269 OutputFormat::Pretty => Box::new(PrettyFormatter::new(
270 output,
271 opts.use_color(),
272 max_name_len,
273 is_multithreaded,
274 opts.time_options,
275 )),
276 OutputFormat::Terse => {
277 Box::new(TerseFormatter::new(output, opts.use_color(), max_name_len, is_multithreaded))
278 }
279 OutputFormat::Json => Box::new(JsonFormatter::new(output)),
280 };
281 let mut st = ConsoleTestState::new(opts)?;
282
283 // Prevent the usage of `Instant` in some cases:
284 // - It's currently not supported for wasm targets.
285 // - We disable it for miri because it's not available when isolation is enabled.
286 let is_instant_supported = !cfg!(target_arch = "wasm32") && !cfg!(miri);
287
288 let start_time = if is_instant_supported { Some(Instant::now()) } else { None };
289 run_tests(opts, tests, |x| on_test_event(&x, &mut st, &mut *out))?;
290 st.exec_time = start_time.map(|t| TestSuiteExecTime(t.elapsed()));
291
292 assert!(st.current_test_count() == st.total);
293
294 out.write_run_finish(&st)
295}
296
297// Calculates padding for given test description.
298fn len_if_padded(t: &TestDescAndFn) -> usize {
299 match t.testfn.padding() {
300 NamePadding::PadNone => 0,
301 NamePadding::PadOnRight => t.desc.name.as_slice().len(),
302 }
303}
304