Skip to main content

libtest_mimic/
lib.rs

1//! Write your own tests and benchmarks that look and behave like built-in tests!
2//!
3//! This is a simple and small test harness that mimics the original `libtest`
4//! (used by `cargo test`/`rustc --test`). That means: all output looks pretty
5//! much like `cargo test` and most CLI arguments are understood and used. With
6//! that plumbing work out of the way, your test runner can focus on the actual
7//! testing.
8//!
9//! For a small real world example, see [`examples/tidy.rs`][1].
10//!
11//! [1]: https://github.com/LukasKalbertodt/libtest-mimic/blob/master/examples/tidy.rs
12//!
13//! # Usage
14//!
15//! To use this, you most likely want to add a manual `[[test]]` section to
16//! `Cargo.toml` and set `harness = false`. For example:
17//!
18//! ```toml
19//! [[test]]
20//! name = "mytest"
21//! path = "tests/mytest.rs"
22//! harness = false
23//! ```
24//!
25//! And in `tests/mytest.rs` you would call [`run`] in the `main` function:
26//!
27//! ```no_run
28//! use libtest_mimic::{Arguments, Trial};
29//!
30//!
31//! // Parse command line arguments
32//! let args = Arguments::from_args();
33//!
34//! // Create a list of tests and/or benchmarks (in this case: two dummy tests).
35//! let tests = vec![
36//!     Trial::test("succeeding_test", move || Ok(())),
37//!     Trial::test("failing_test", move || Err("Woops".into())),
38//! ];
39//!
40//! // Run all tests and exit the application appropriatly.
41//! libtest_mimic::run(&args, tests).exit();
42//! ```
43//!
44//! Instead of returning `Ok` or `Err` directly, you want to actually perform
45//! your tests, of course. See [`Trial::test`] for more information on how to
46//! define a test. You can of course list all your tests manually. But in many
47//! cases it is useful to generate one test per file in a directory, for
48//! example.
49//!
50//! You can then run `cargo test --test mytest` to run it. To see the CLI
51//! arguments supported by this crate, run `cargo test --test mytest -- -h`.
52//!
53//!
54//! # Known limitations and differences to the official test harness
55//!
56//! `libtest-mimic` works on a best-effort basis: it tries to be as close to
57//! `libtest` as possible, but there are differences for a variety of reasons.
58//! For example, some rarely used features might not be implemented, some
59//! features are extremely difficult to implement, and removing minor,
60//! unimportant differences is just not worth the hassle.
61//!
62//! Some of the notable differences:
63//!
64//! - Output capture and `--nocapture`: simply not supported. The official
65//!   `libtest` uses internal `std` functions to temporarily redirect output.
66//!   `libtest-mimic` cannot use those. See [this issue][capture] for more
67//!   information.
68//! - `--format=junit`
69//! - Also see [#13](https://github.com/LukasKalbertodt/libtest-mimic/issues/13)
70//!
71//! [capture]: https://github.com/LukasKalbertodt/libtest-mimic/issues/9
72
73#![forbid(unsafe_code)]
74
75use std::{
76    borrow::Cow,
77    fmt,
78    process::{self, ExitCode},
79    sync::{mpsc, Mutex},
80    thread,
81    time::Instant,
82};
83
84mod args;
85mod printer;
86
87use printer::Printer;
88
89pub use crate::args::{Arguments, ColorSetting, FormatSetting};
90
91
92
93/// A single test or benchmark.
94///
95/// The original `libtest` often calls benchmarks "tests", which is a bit
96/// confusing. So in this library, it is called "trial".
97///
98/// A trial is created via [`Trial::test`] or [`Trial::bench`]. The trial's
99/// `name` is printed and used for filtering. The `runner` is called when the
100/// test/benchmark is executed to determine its outcome. If `runner` panics,
101/// the trial is considered "failed". If you need the behavior of
102/// `#[should_panic]` you need to catch the panic yourself. You likely want to
103/// compare the panic payload to an expected value anyway.
104pub struct Trial {
105    runner: Box<dyn FnOnce(bool) -> Outcome + Send>,
106    info: TestInfo,
107}
108
109/// A representation of whether a test ran to completion or was ignored during its runtime.
110pub enum Completion {
111    /// Test completed successfully.
112    Completed,
113
114    /// Test was ignored.
115    Ignored { reason: Option<String> },
116}
117
118impl Completion {
119    /// Returns `Self::Ignored` without reason.
120    pub fn ignored() -> Self {
121        Self::Ignored { reason: None }
122    }
123
124    /// Returns `Self::Ignored` with the given reason.
125    pub fn ignored_with(reason: impl ToString) -> Self {
126        Self::Ignored { reason: Some(reason.to_string()) }
127    }
128}
129
130impl Trial {
131    /// Creates a (non-benchmark) test with the given name and runner.
132    ///
133    /// The runner returning `Ok(())` is interpreted as the test passing. If the
134    /// runner returns `Err(_)`, the test is considered failed.
135    pub fn test<R>(name: impl Into<String>, runner: R) -> Self
136    where
137        R: FnOnce() -> Result<(), Failed> + Send + 'static,
138    {
139        Self::ignorable_test(name, || runner().map(|()| Completion::Completed))
140    }
141
142    /// Creates a test like [`Self::test`], but with a runner that can decide to
143    /// ignore the test.
144    ///
145    /// Like other tests, returning an `Err` is a test failure. The `Ok` variant for this test must
146    /// return a [`Completion`] to indicate whether the test successfully ran to completion, or if
147    /// it was ignored at some point during testing. If it was skipped, a reason may be provided.
148    pub fn ignorable_test<R>(name: impl Into<String>, runner: R) -> Self
149    where
150        R: FnOnce() -> Result<Completion, Failed> + Send + 'static,
151    {
152        Self {
153            runner: Box::new(|_test_mode| match runner() {
154                Ok(Completion::Completed) => Outcome::Passed,
155                Ok(Completion::Ignored { reason }) => Outcome::RuntimeIgnored { reason },
156                Err(e) => Outcome::Failed(e),
157            }),
158            info: TestInfo {
159                name: name.into(),
160                kind: String::new(),
161                is_ignored: false,
162                is_bench: false,
163            },
164        }
165    }
166
167    /// Creates a benchmark with the given name and runner.
168    ///
169    /// If the runner's parameter `test_mode` is `true`, the runner function
170    /// should run all code just once, without measuring, just to make sure it
171    /// does not panic. If the parameter is `false`, it should perform the
172    /// actual benchmark. If `test_mode` is `true` you may return `Ok(None)`,
173    /// but if it's `false`, you have to return a `Measurement`, or else the
174    /// benchmark is considered a failure.
175    ///
176    /// `test_mode` is `true` if neither `--bench` nor `--test` are set, and
177    /// `false` when `--bench` is set. If `--test` is set, benchmarks are not
178    /// ran at all, and both flags cannot be set at the same time.
179    pub fn bench<R>(name: impl Into<String>, runner: R) -> Self
180    where
181        R: FnOnce(bool) -> Result<Option<Measurement>, Failed> + Send + 'static,
182    {
183        Self {
184            runner: Box::new(move |test_mode| match runner(test_mode) {
185                Err(failed) => Outcome::Failed(failed),
186                Ok(_) if test_mode => Outcome::Passed,
187                Ok(Some(measurement)) => Outcome::Measured(measurement),
188                Ok(None)
189                    => Outcome::Failed("bench runner returned `Ok(None)` in bench mode".into()),
190            }),
191            info: TestInfo {
192                name: name.into(),
193                kind: String::new(),
194                is_ignored: false,
195                is_bench: true,
196            },
197        }
198    }
199
200    /// Sets the "kind" of this test/benchmark. If this string is not
201    /// empty, it is printed in brackets before the test name (e.g.
202    /// `test [my-kind] test_name`). (Default: *empty*)
203    ///
204    /// This is the only extension to the original libtest.
205    pub fn with_kind(self, kind: impl Into<String>) -> Self {
206        Self {
207            info: TestInfo {
208                kind: kind.into(),
209                ..self.info
210            },
211            ..self
212        }
213    }
214
215    /// Sets whether or not this test is considered "ignored". (Default: `false`)
216    ///
217    /// With the built-in test suite, you can annotate `#[ignore]` on tests to
218    /// not execute them by default (for example because they take a long time
219    /// or require a special environment). If the `--ignored` flag is set,
220    /// ignored tests are executed, too.
221    pub fn with_ignored_flag(self, is_ignored: bool) -> Self {
222        Self {
223            info: TestInfo {
224                is_ignored,
225                ..self.info
226            },
227            ..self
228        }
229    }
230
231    /// Returns the name of this trial.
232    pub fn name(&self) -> &str {
233        &self.info.name
234    }
235
236    /// Returns the kind of this trial. If you have not set a kind, this is an
237    /// empty string.
238    pub fn kind(&self) -> &str {
239        &self.info.kind
240    }
241
242    /// Returns whether this trial has been marked as *ignored*.
243    pub fn has_ignored_flag(&self) -> bool {
244        self.info.is_ignored
245    }
246
247    /// Returns `true` iff this trial is a test (as opposed to a benchmark).
248    pub fn is_test(&self) -> bool {
249        !self.info.is_bench
250    }
251
252    /// Returns `true` iff this trial is a benchmark (as opposed to a test).
253    pub fn is_bench(&self) -> bool {
254        self.info.is_bench
255    }
256}
257
258impl fmt::Debug for Trial {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        struct OpaqueRunner;
261        impl fmt::Debug for OpaqueRunner {
262            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263                f.write_str("<runner>")
264            }
265        }
266
267        f.debug_struct("Test")
268            .field("runner", &OpaqueRunner)
269            .field("name", &self.info.name)
270            .field("kind", &self.info.kind)
271            .field("is_ignored", &self.info.is_ignored)
272            .field("is_bench", &self.info.is_bench)
273            .finish()
274    }
275}
276
277#[derive(Debug)]
278struct TestInfo {
279    name: String,
280    kind: String,
281    is_ignored: bool,
282    is_bench: bool,
283}
284
285impl TestInfo {
286    fn test_name_with_kind(&self) -> Cow<'_, str> {
287        if self.kind.is_empty() {
288            Cow::Borrowed(&self.name)
289        } else {
290            Cow::Owned(format!("[{}] {}", self.kind, self.name))
291        }
292    }
293}
294
295/// Output of a benchmark.
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub struct Measurement {
298    /// Average time in ns.
299    pub avg: u64,
300
301    /// Variance in ns.
302    pub variance: u64,
303}
304
305/// Indicates that a test/benchmark has failed. Optionally carries a message.
306///
307/// You usually want to use the `From` impl of this type, which allows you to
308/// convert any `T: fmt::Display` (e.g. `String`, `&str`, ...) into `Failed`.
309#[derive(Debug, Clone)]
310pub struct Failed {
311    msg: Option<String>,
312}
313
314impl Failed {
315    /// Creates an instance without message.
316    pub fn without_message() -> Self {
317        Self { msg: None }
318    }
319
320    /// Returns the message of this instance.
321    pub fn message(&self) -> Option<&str> {
322        self.msg.as_deref()
323    }
324}
325
326impl<M: std::fmt::Display> From<M> for Failed {
327    fn from(msg: M) -> Self {
328        Self {
329            msg: Some(msg.to_string())
330        }
331    }
332}
333
334
335
336/// The outcome of performing a test/benchmark.
337#[derive(Debug, Clone)]
338enum Outcome {
339    /// The test passed.
340    Passed,
341
342    /// The test or benchmark failed.
343    Failed(Failed),
344
345    /// The test or benchmark was ignored.
346    Ignored,
347
348    /// The test or benchmark was ignored.
349    RuntimeIgnored { reason: Option<String> },
350
351    /// The benchmark was successfully run.
352    Measured(Measurement),
353}
354
355/// Contains information about the entire test run. Is returned by[`run`].
356///
357/// This type is marked as `#[must_use]`. Usually, you just call
358/// [`exit()`][Conclusion::exit] on the result of `run` to exit the application
359/// with the correct exit code. But you can also store this value and inspect
360/// its data.
361#[derive(Clone, Debug, PartialEq, Eq)]
362#[must_use = "Call `exit()` or `exit_if_failed()` to set the correct return code"]
363pub struct Conclusion {
364    /// Number of tests and benchmarks that were filtered out (either by the
365    /// filter-in pattern or by `--skip` arguments).
366    pub num_filtered_out: u64,
367
368    /// Number of passed tests.
369    pub num_passed: u64,
370
371    /// Number of failed tests and benchmarks.
372    pub num_failed: u64,
373
374    /// Number of ignored tests and benchmarks.
375    pub num_ignored: u64,
376
377    /// Number of benchmarks that successfully ran.
378    pub num_measured: u64,
379}
380
381impl Conclusion {
382    /// Returns an exit code that can be returned from `main` to signal
383    /// success/failure to the calling process.
384    pub fn exit_code(&self) -> ExitCode {
385        if self.has_failed() {
386            ExitCode::from(101)
387        } else {
388            ExitCode::SUCCESS
389        }
390    }
391
392    /// Returns whether there have been any failures.
393    pub fn has_failed(&self) -> bool {
394        self.num_failed > 0
395    }
396
397    /// Exits the application with an appropriate error code (0 if all tests
398    /// have passed, 101 if there have been failures). This uses
399    /// [`process::exit`], meaning that destructors are not ran. Consider
400    /// using [`Self::exit_code`] instead for a proper program cleanup.
401    pub fn exit(&self) -> ! {
402        self.exit_if_failed();
403        process::exit(0);
404    }
405
406    /// Exits the application with error code 101 if there were any failures.
407    /// Otherwise, returns normally. This uses [`process::exit`], meaning that
408    /// destructors are not ran. Consider using [`Self::exit_code`] instead for
409    /// a proper program cleanup.
410    pub fn exit_if_failed(&self) {
411        if self.has_failed() {
412            process::exit(101)
413        }
414    }
415
416    fn empty() -> Self {
417        Self {
418            num_filtered_out: 0,
419            num_passed: 0,
420            num_failed: 0,
421            num_ignored: 0,
422            num_measured: 0,
423        }
424    }
425}
426
427impl Arguments {
428    /// Returns `true` if the given trial should be ignored by these arguments.
429    ///
430    /// Ignored tests are not run, but still listed in the outcome.
431    pub fn is_ignored(&self, test: &Trial) -> bool {
432        (test.info.is_ignored && !self.ignored && !self.include_ignored)
433            || (test.info.is_bench && self.test)
434            || (!test.info.is_bench && self.bench)
435    }
436
437    /// Returns `true` if the given trial should be filtered out by these
438    /// arguments.
439    pub fn is_filtered_out(&self, test: &Trial) -> bool {
440        let test_name = test.name();
441        // Match against the full test name, including the kind. This upholds the invariant that if
442        // --list prints out:
443        //
444        // <some string>: test
445        //
446        // then "--exact <some string>" runs exactly that test.
447        let test_name_with_kind = test.info.test_name_with_kind();
448
449        // If a filter was specified, apply this
450        if let Some(filter) = &self.filter {
451            match self.exact {
452                // For exact matches, we want to match against either the test name (to maintain
453                // backwards compatibility with older versions of libtest-mimic), or the test kind
454                // (technically more correct with respect to matching against the output of --list.)
455                true if test_name != filter && &test_name_with_kind != filter => return true,
456                false if !test_name_with_kind.contains(filter) => return true,
457                _ => {}
458            };
459        }
460
461        // If any skip pattern were specified, test for all patterns.
462        for skip_filter in &self.skip {
463            match self.exact {
464                // For exact matches, we want to match against either the test name (to maintain
465                // backwards compatibility with older versions of libtest-mimic), or the test kind
466                // (technically more correct with respect to matching against the output of --list.)
467                true if test_name == skip_filter || &test_name_with_kind == skip_filter => {
468                    return true
469                }
470                false if test_name_with_kind.contains(skip_filter) => return true,
471                _ => {}
472            }
473        }
474
475        if self.ignored && !test.info.is_ignored {
476            return true;
477        }
478
479        false
480    }
481}
482
483/// Runs all given trials (tests & benchmarks).
484///
485/// This is the central function of this crate. It provides the framework for
486/// the testing harness. It does all the printing and house keeping.
487///
488/// The returned value contains a couple of useful information. See
489/// [`Conclusion`] for more information. If `--list` was specified, a list is
490/// printed and a dummy `Conclusion` is returned.
491pub fn run(args: &Arguments, mut tests: Vec<Trial>) -> Conclusion {
492    let start_instant = Instant::now();
493    let mut conclusion = Conclusion::empty();
494
495    // Apply filtering
496    if args.filter.is_some() || !args.skip.is_empty() || args.ignored {
497        let len_before = tests.len() as u64;
498        tests.retain(|test| !args.is_filtered_out(test));
499        conclusion.num_filtered_out = len_before - tests.len() as u64;
500    }
501    let tests = tests;
502
503    // Create printer which is used for all output.
504    let mut printer = printer::Printer::new(args, &tests);
505
506    // If `--list` is specified, just print the list and return.
507    if args.list {
508        printer.print_list(&tests, args.ignored);
509        return Conclusion::empty();
510    }
511
512    // Print number of tests
513    printer.print_title(tests.len() as u64);
514
515    let mut failed_tests = Vec::new();
516    let mut handle_outcome = |outcome: Outcome, test: TestInfo, printer: &mut Printer| {
517        printer.print_single_outcome(&test, &outcome);
518
519        // Handle outcome
520        match outcome {
521            Outcome::Passed => conclusion.num_passed += 1,
522            Outcome::Failed(failed) => {
523                failed_tests.push((test, failed.msg));
524                conclusion.num_failed += 1;
525            }
526            Outcome::Ignored => conclusion.num_ignored += 1,
527            Outcome::Measured(_) => conclusion.num_measured += 1,
528            Outcome::RuntimeIgnored { .. } => conclusion.num_ignored += 1,
529        }
530    };
531
532    // Execute all tests.
533    let test_mode = !args.bench;
534
535    let num_threads = platform_defaults_to_one_thread()
536        .then_some(1)
537        .or(args.test_threads)
538        .or_else(|| std::thread::available_parallelism().ok().map(Into::into))
539        .unwrap_or(1);
540
541    if num_threads == 1 {
542        // Run test sequentially in main thread
543        for test in tests {
544            // Print `test foo    ...`, run the test, then print the outcome in
545            // the same line.
546            printer.print_test(&test.info);
547            let outcome = if args.is_ignored(&test) {
548                Outcome::Ignored
549            } else {
550                run_single(test.runner, test_mode)
551            };
552            handle_outcome(outcome, test.info, &mut printer);
553        }
554    } else {
555        // Run test in thread pool.
556        let (sender, receiver) = mpsc::channel();
557
558        let num_tests = tests.len();
559        // TODO: this should use a mpmc channel, once that's stabilized in std.
560        let iter = Mutex::new(tests.into_iter());
561        thread::scope(|scope| {
562            // Start worker threads
563            for _ in 0..num_threads {
564                scope.spawn(|| {
565                    loop {
566                        // Get next test to process from the iterator.
567                        let Some(trial) = iter.lock().unwrap().next() else {
568                            break;
569                        };
570
571                        let payload = if args.is_ignored(&trial) {
572                            (Outcome::Ignored, trial.info)
573                        } else {
574                            let outcome = run_single(trial.runner, test_mode);
575                            (outcome, trial.info)
576                        };
577
578                        // It's fine to ignore the result of sending. If the
579                        // receiver has hung up, everything will wind down soon
580                        // anyway.
581                        let _ = sender.send(payload);
582                    }
583                });
584            }
585
586            // Print results of tests that already dinished
587            for (outcome, test_info) in receiver.iter().take(num_tests) {
588                // In multithreaded mode, we do only print the start of the line
589                // after the test ran, as otherwise it would lead to terribly
590                // interleaved output.
591                printer.print_test(&test_info);
592                handle_outcome(outcome, test_info, &mut printer);
593            }
594        });
595
596    }
597
598    // Print failures if there were any, and the final summary.
599    if !failed_tests.is_empty() {
600        printer.print_failures(&failed_tests);
601    }
602
603    printer.print_summary(&conclusion, start_instant.elapsed());
604
605    conclusion
606}
607
608/// Returns whether the current host platform should use a single thread by
609/// default rather than a thread pool by default. Some platforms, such as
610/// WebAssembly, don't have native support for threading at this time.
611fn platform_defaults_to_one_thread() -> bool {
612    cfg!(target_family = "wasm")
613}
614
615/// Runs the given runner, catching any panics and treating them as a failed test.
616fn run_single(runner: Box<dyn FnOnce(bool) -> Outcome + Send>, test_mode: bool) -> Outcome {
617    use std::panic::{catch_unwind, AssertUnwindSafe};
618
619    catch_unwind(AssertUnwindSafe(move || runner(test_mode))).unwrap_or_else(|e| {
620        // The `panic` information is just an `Any` object representing the
621        // value the panic was invoked with. For most panics (which use
622        // `panic!` like `println!`), this is either `&str` or `String`.
623        let payload = e.downcast_ref::<String>()
624            .map(|s| s.as_str())
625            .or(e.downcast_ref::<&str>().map(|s| *s));
626
627        let msg = match payload {
628            Some(payload) => format!("test panicked: {payload}"),
629            None => format!("test panicked"),
630        };
631        Outcome::Failed(msg.into())
632    })
633}