//! 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 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(name: impl Into, 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(name: impl Into, runner: R) -> Self where R: FnOnce(bool) -> Result, 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) -> 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("") } } 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, } 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 From 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) -> 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 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::() .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()) }) }