af_core/test/
runner.rs

1// Copyright © 2021 Alexandra Frydl
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Runs tests concurrently with a progress bar.
8
9pub use af_core_macros::test_main as main;
10
11use crate::test::prelude::*;
12use crate::util::defer;
13use console::style;
14use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
15use std::io;
16
17/// A test runner error.
18#[derive(Debug, Error)]
19pub enum Error {
20  /// One or more tests failed.
21  #[error("{}.", fmt::count(*.0, "test failure", "test failures"))]
22  Failures(usize),
23  /// An IO error occurred writing to `stdout`.
24  #[error("IO error. {0}")]
25  Io(#[from] io::Error),
26}
27
28/// The output of the test runner.
29pub struct Output {
30  pub elapsed: Duration,
31  pub failures: usize,
32  pub tests: usize,
33}
34
35/// A test runner result.
36pub type Result<T = Output, E = Error> = std::result::Result<T, E>;
37
38/// Runs a test context in the default test runner.
39pub async fn run(build: impl FnOnce(&mut test::Context)) -> Result {
40  let style_initial = ProgressStyle::default_bar()
41    .template("[{elapsed_precise}] {bar:40} {pos:>7}/{len:7} {msg}")
42    .progress_chars("##-");
43
44  let style_ok = style_initial
45    .clone()
46    .template("[{elapsed_precise}] {bar:40.green.bright/.green} {pos:>7}/{len:7} {msg}");
47
48  let style_err = style_ok
49    .clone()
50    .template("[{elapsed_precise}] {bar:40.red.bright/.red} {pos:>7}/{len:7} {msg}");
51
52  let mut term = console::Term::buffered_stdout();
53  let pb = ProgressBar::with_draw_target(0, ProgressDrawTarget::to_term(term.clone(), None));
54
55  pb.set_message("Starting…");
56  pb.set_style(style_initial);
57
58  let mut ctx = test::Context::new();
59
60  build(&mut ctx);
61
62  let panic_hook = panic::take_hook();
63  let _guard = defer(|| panic::set_hook(panic_hook));
64
65  panic::set_hook(Box::new(|_| ()));
66
67  let started_at = Time::now();
68  let mut output = ctx.start();
69
70  let tests = output.len();
71  let mut failures = 0;
72
73  pb.set_length(tests as u64);
74  pb.set_message("Running…");
75  pb.set_style(style_ok);
76
77  while let Some(test::Output { path, result }) = output.next().await {
78    if let Err(err) = result {
79      failures += 1;
80
81      term.clear_last_lines(1)?;
82
83      pb.set_style(style_err.clone());
84
85      writeln!(
86        term,
87        "{} {} — {:#}\n",
88        path,
89        style("failed").bright().red(),
90        fmt::indent("", "  ", err)
91      )?;
92    }
93
94    pb.set_position((tests - output.len()) as u64);
95  }
96
97  pb.finish_and_clear();
98
99  let elapsed = started_at.elapsed();
100
101  let (count, status) = match failures {
102    0 => (fmt::count(tests, "test", "tests"), style("passed").bright().green()),
103    n => (fmt::count(n, "test", "tests"), style("failed").bright().red()),
104  };
105
106  if failures > 0 {
107    writeln!(term)?;
108  }
109
110  writeln!(term, "{} {} in {}.", count, status, style(elapsed).bright().white())?;
111
112  term.flush()?;
113
114  Ok(Output { elapsed, failures, tests })
115}