blob: 634ce894afc47139145ab77fbedbdf60478d2cf4 [file] [log] [blame] [edit]
//! Write your own tests and benchmarks that look and behave like built-in tests!
//!
//! This is a simple and small test harness that mimics the original `libtest`
//! (used by `cargo test`/`rustc --test`). That means: all output looks pretty
//! much like `cargo test` and most CLI arguments are understood and used. With
//! that plumbing work out of the way, your test runner can focus on the actual
//! testing.
//!
//! For a small real world example, see [`examples/tidy.rs`][1].
//!
//! [1]: https://github.com/LukasKalbertodt/libtest-mimic/blob/master/examples/tidy.rs
//!
//! # Usage
//!
//! To use this, you most likely want to add a manual `[[test]]` section to
//! `Cargo.toml` and set `harness = false`. For example:
//!
//! ```toml
//! [[test]]
//! name = "mytest"
//! path = "tests/mytest.rs"
//! harness = false
//! ```
//!
//! And in `tests/mytest.rs` you would call [`run`] in the `main` function:
//!
//! ```no_run
//! use libtest_mimic::{Arguments, Trial};
//!
//!
//! // Parse command line arguments
//! let args = Arguments::from_args();
//!
//! // Create a list of tests and/or benchmarks (in this case: two dummy tests).
//! let tests = vec![
//! Trial::test("succeeding_test", move || Ok(())),
//! Trial::test("failing_test", move || Err("Woops".into())),
//! ];
//!
//! // Run all tests and exit the application appropriatly.
//! libtest_mimic::run(&args, tests).exit();
//! ```
//!
//! Instead of returning `Ok` or `Err` directly, you want to actually perform
//! your tests, of course. See [`Trial::test`] for more information on how to
//! define a test. You can of course list all your tests manually. But in many
//! cases it is useful to generate one test per file in a directory, for
//! example.
//!
//! You can then run `cargo test --test mytest` to run it. To see the CLI
//! arguments supported by this crate, run `cargo test --test mytest -- -h`.
//!
//!
//! # Known limitations and differences to the official test harness
//!
//! `libtest-mimic` works on a best-effort basis: it tries to be as close to
//! `libtest` as possible, but there are differences for a variety of reasons.
//! For example, some rarely used features might not be implemented, some
//! features are extremely difficult to implement, and removing minor,
//! unimportant differences is just not worth the hassle.
//!
//! Some of the notable differences:
//!
//! - Output capture and `--nocapture`: simply not supported. The official
//! `libtest` uses internal `std` functions to temporarily redirect output.
//! `libtest-mimic` cannot use those. See [this issue][capture] for more
//! information.
//! - `--format=json|junit`
//!
//! [capture]: https://github.com/LukasKalbertodt/libtest-mimic/issues/9
use std::{process, sync::mpsc, fmt, time::Instant};
mod args;
mod printer;
use printer::Printer;
use threadpool::ThreadPool;
pub use crate::args::{Arguments, ColorSetting, FormatSetting};
/// A single test or benchmark.
///
/// `libtest` often treats benchmarks as "tests", which is a bit confusing. So
/// in this library, it is called "trial".
///
/// A trial is create via [`Trial::test`] or [`Trial::bench`]. The trial's
/// `name` is printed and used for filtering. The `runner` is called when the
/// test/benchmark is executed to determine its outcome. If `runner` panics,
/// the trial is considered "failed". If you need the behavior of
/// `#[should_panic]` you need to catch the panic yourself. You likely want to
/// compare the panic payload to an expected value anyway.
pub struct Trial {
runner: Box<dyn FnOnce(bool) -> Outcome + Send>,
info: TestInfo,
}
impl Trial {
/// Creates a (non-benchmark) test with the given name and runner.
///
/// The runner returning `Ok(())` is interpreted as the test passing. If the
/// runner returns `Err(_)`, the test is considered failed.
pub fn test<R>(name: impl Into<String>, runner: R) -> Self
where
R: FnOnce() -> Result<(), Failed> + Send + 'static,
{
Self {
runner: Box::new(move |_test_mode| match runner() {
Ok(()) => Outcome::Passed,
Err(failed) => Outcome::Failed(failed),
}),
info: TestInfo {
name: name.into(),
kind: String::new(),
is_ignored: false,
is_bench: false,
},
}
}
/// Creates a benchmark with the given name and runner.
///
/// If the runner's parameter `test_mode` is `true`, the runner function
/// should run all code just once, without measuring, just to make sure it
/// does not panic. If the parameter is `false`, it should perform the
/// actual benchmark. If `test_mode` is `true` you may return `Ok(None)`,
/// but if it's `false`, you have to return a `Measurement`, or else the
/// benchmark is considered a failure.
///
/// `test_mode` is `true` if neither `--bench` nor `--test` are set, and
/// `false` when `--bench` is set. If `--test` is set, benchmarks are not
/// ran at all, and both flags cannot be set at the same time.
pub fn bench<R>(name: impl Into<String>, runner: R) -> Self
where
R: FnOnce(bool) -> Result<Option<Measurement>, Failed> + Send + 'static,
{
Self {
runner: Box::new(move |test_mode| match runner(test_mode) {
Err(failed) => Outcome::Failed(failed),
Ok(_) if test_mode => Outcome::Passed,
Ok(Some(measurement)) => Outcome::Measured(measurement),
Ok(None)
=> Outcome::Failed("bench runner returned `Ok(None)` in bench mode".into()),
}),
info: TestInfo {
name: name.into(),
kind: String::new(),
is_ignored: false,
is_bench: true,
},
}
}
/// Sets the "kind" of this test/benchmark. If this string is not
/// empty, it is printed in brackets before the test name (e.g.
/// `test [my-kind] test_name`). (Default: *empty*)
///
/// This is the only extension to the original libtest.
pub fn with_kind(self, kind: impl Into<String>) -> Self {
Self {
info: TestInfo {
kind: kind.into(),
..self.info
},
..self
}
}
/// Sets whether or not this test is considered "ignored". (Default: `false`)
///
/// With the built-in test suite, you can annotate `#[ignore]` on tests to
/// not execute them by default (for example because they take a long time
/// or require a special environment). If the `--ignored` flag is set,
/// ignored tests are executed, too.
pub fn with_ignored_flag(self, is_ignored: bool) -> Self {
Self {
info: TestInfo {
is_ignored,
..self.info
},
..self
}
}
/// Returns the name of this trial.
pub fn name(&self) -> &str {
&self.info.name
}
/// Returns the kind of this trial. If you have not set a kind, this is an
/// empty string.
pub fn kind(&self) -> &str {
&self.info.kind
}
/// Returns whether this trial has been marked as *ignored*.
pub fn has_ignored_flag(&self) -> bool {
self.info.is_ignored
}
/// Returns `true` iff this trial is a test (as opposed to a benchmark).
pub fn is_test(&self) -> bool {
!self.info.is_bench
}
/// Returns `true` iff this trial is a benchmark (as opposed to a test).
pub fn is_bench(&self) -> bool {
self.info.is_bench
}
}
impl fmt::Debug for Trial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct OpaqueRunner;
impl fmt::Debug for OpaqueRunner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<runner>")
}
}
f.debug_struct("Test")
.field("runner", &OpaqueRunner)
.field("name", &self.info.name)
.field("kind", &self.info.kind)
.field("is_ignored", &self.info.is_ignored)
.field("is_bench", &self.info.is_bench)
.finish()
}
}
#[derive(Debug)]
struct TestInfo {
name: String,
kind: String,
is_ignored: bool,
is_bench: bool,
}
/// Output of a benchmark.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Measurement {
/// Average time in ns.
pub avg: u64,
/// Variance in ns.
pub variance: u64,
}
/// Indicates that a test/benchmark has failed. Optionally carries a message.
///
/// You usually want to use the `From` impl of this type, which allows you to
/// convert any `T: fmt::Display` (e.g. `String`, `&str`, ...) into `Failed`.
#[derive(Debug, Clone)]
pub struct Failed {
msg: Option<String>,
}
impl Failed {
/// Creates an instance without message.
pub fn without_message() -> Self {
Self { msg: None }
}
/// Returns the message of this instance.
pub fn message(&self) -> Option<&str> {
self.msg.as_deref()
}
}
impl<M: std::fmt::Display> From<M> for Failed {
fn from(msg: M) -> Self {
Self {
msg: Some(msg.to_string())
}
}
}
/// The outcome of performing a test/benchmark.
#[derive(Debug, Clone)]
enum Outcome {
/// The test passed.
Passed,
/// The test or benchmark failed.
Failed(Failed),
/// The test or benchmark was ignored.
Ignored,
/// The benchmark was successfully run.
Measured(Measurement),
}
/// Contains information about the entire test run. Is returned by[`run`].
///
/// This type is marked as `#[must_use]`. Usually, you just call
/// [`exit()`][Conclusion::exit] on the result of `run` to exit the application
/// with the correct exit code. But you can also store this value and inspect
/// its data.
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use = "Call `exit()` or `exit_if_failed()` to set the correct return code"]
pub struct Conclusion {
/// Number of tests and benchmarks that were filtered out (either by the
/// filter-in pattern or by `--skip` arguments).
pub num_filtered_out: u64,
/// Number of passed tests.
pub num_passed: u64,
/// Number of failed tests and benchmarks.
pub num_failed: u64,
/// Number of ignored tests and benchmarks.
pub num_ignored: u64,
/// Number of benchmarks that successfully ran.
pub num_measured: u64,
}
impl Conclusion {
/// Exits the application with an appropriate error code (0 if all tests
/// have passed, 101 if there have been failures).
pub fn exit(&self) -> ! {
self.exit_if_failed();
process::exit(0);
}
/// Exits the application with error code 101 if there were any failures.
/// Otherwise, returns normally.
pub fn exit_if_failed(&self) {
if self.has_failed() {
process::exit(101)
}
}
/// Returns whether there have been any failures.
pub fn has_failed(&self) -> bool {
self.num_failed > 0
}
fn empty() -> Self {
Self {
num_filtered_out: 0,
num_passed: 0,
num_failed: 0,
num_ignored: 0,
num_measured: 0,
}
}
}
impl Arguments {
/// Returns `true` if the given test should be ignored.
fn is_ignored(&self, test: &Trial) -> bool {
(test.info.is_ignored && !self.ignored && !self.include_ignored)
|| (test.info.is_bench && self.test)
|| (!test.info.is_bench && self.bench)
}
fn is_filtered_out(&self, test: &Trial) -> bool {
let test_name = &test.info.name;
// If a filter was specified, apply this
if let Some(filter) = &self.filter {
match self.exact {
true if test_name != filter => return true,
false if !test_name.contains(filter) => return true,
_ => {}
};
}
// If any skip pattern were specified, test for all patterns.
for skip_filter in &self.skip {
match self.exact {
true if test_name == skip_filter => return true,
false if test_name.contains(skip_filter) => return true,
_ => {}
}
}
if self.ignored && !test.info.is_ignored {
return true;
}
false
}
}
/// Runs all given tests.
///
/// This is the central function of this crate. It provides the framework for
/// the testing harness. It does all the printing and house keeping.
///
/// The returned value contains a couple of useful information. See
/// [`Conclusion`] for more information. If `--list` was specified, a list is
/// printed and a dummy `Conclusion` is returned.
pub fn run(args: &Arguments, mut tests: Vec<Trial>) -> Conclusion {
let start_instant = Instant::now();
let mut conclusion = Conclusion::empty();
// Apply filtering
if args.filter.is_some() || !args.skip.is_empty() || args.ignored {
let len_before = tests.len() as u64;
tests.retain(|test| !args.is_filtered_out(test));
conclusion.num_filtered_out = len_before - tests.len() as u64;
}
let tests = tests;
// Create printer which is used for all output.
let mut printer = printer::Printer::new(args, &tests);
// If `--list` is specified, just print the list and return.
if args.list {
printer.print_list(&tests, args.ignored);
return Conclusion::empty();
}
// Print number of tests
printer.print_title(tests.len() as u64);
let mut failed_tests = Vec::new();
let mut handle_outcome = |outcome: Outcome, test: TestInfo, printer: &mut Printer| {
printer.print_single_outcome(&outcome);
// Handle outcome
match outcome {
Outcome::Passed => conclusion.num_passed += 1,
Outcome::Failed(failed) => {
failed_tests.push((test, failed.msg));
conclusion.num_failed += 1;
},
Outcome::Ignored => conclusion.num_ignored += 1,
Outcome::Measured(_) => conclusion.num_measured += 1,
}
};
// Execute all tests.
let test_mode = !args.bench;
if args.test_threads == Some(1) {
// Run test sequentially in main thread
for test in tests {
// Print `test foo ...`, run the test, then print the outcome in
// the same line.
printer.print_test(&test.info);
let outcome = if args.is_ignored(&test) {
Outcome::Ignored
} else {
run_single(test.runner, test_mode)
};
handle_outcome(outcome, test.info, &mut printer);
}
} else {
// Run test in thread pool.
let pool = ThreadPool::default();
let (sender, receiver) = mpsc::channel();
let num_tests = tests.len();
for test in tests {
if args.is_ignored(&test) {
sender.send((Outcome::Ignored, test.info)).unwrap();
} else {
let sender = sender.clone();
pool.execute(move || {
// It's fine to ignore the result of sending. If the
// receiver has hung up, everything will wind down soon
// anyway.
let outcome = run_single(test.runner, test_mode);
let _ = sender.send((outcome, test.info));
});
}
}
for (outcome, test_info) in receiver.iter().take(num_tests) {
// In multithreaded mode, we do only print the start of the line
// after the test ran, as otherwise it would lead to terribly
// interleaved output.
printer.print_test(&test_info);
handle_outcome(outcome, test_info, &mut printer);
}
}
// Print failures if there were any, and the final summary.
if !failed_tests.is_empty() {
printer.print_failures(&failed_tests);
}
printer.print_summary(&conclusion, start_instant.elapsed());
conclusion
}
/// Runs the given runner, catching any panics and treating them as a failed test.
fn run_single(runner: Box<dyn FnOnce(bool) -> Outcome + Send>, test_mode: bool) -> Outcome {
use std::panic::{catch_unwind, AssertUnwindSafe};
catch_unwind(AssertUnwindSafe(move || runner(test_mode))).unwrap_or_else(|e| {
// The `panic` information is just an `Any` object representing the
// value the panic was invoked with. For most panics (which use
// `panic!` like `println!`), this is either `&str` or `String`.
let payload = e.downcast_ref::<String>()
.map(|s| s.as_str())
.or(e.downcast_ref::<&str>().map(|s| *s));
let msg = match payload {
Some(payload) => format!("test panicked: {payload}"),
None => format!("test panicked"),
};
Outcome::Failed(msg.into())
})
}