libtest2_mimic/
lib.rs

1//! An experimental replacement for
2//! [libtest-mimic](https://docs.rs/libtest-mimic/latest/libtest_mimic/)
3//!
4//! Write your own tests that look and behave like built-in tests!
5//!
6//! This is a simple and small test harness that mimics the original `libtest`
7//! (used by `cargo test`/`rustc --test`). That means: all output looks pretty
8//! much like `cargo test` and most CLI arguments are understood and used. With
9//! that plumbing work out of the way, your test runner can focus on the actual
10//! testing.
11//!
12//! For a small real world example, see [`examples/mimic-tidy.rs`][1].
13//!
14//! [1]: https://github.com/assert-rs/libtest2/blob/main/crates/libtest2-mimic/examples/mimic-tidy.rs
15//!
16//! # Usage
17//!
18//! To use this, you most likely want to add a manual `[[test]]` section to
19//! `Cargo.toml` and set `harness = false`. For example:
20//!
21//! ```toml
22//! [[test]]
23//! name = "mytest"
24//! path = "tests/mytest.rs"
25//! harness = false
26//! ```
27//!
28//! And in `tests/mytest.rs` you would call [`Harness::main`] in the `main` function:
29//!
30//! ```no_run
31//! # use libtest2_mimic::Trial;
32//! # use libtest2_mimic::Harness;
33//! # use libtest2_mimic::RunError;
34//! Harness::with_env()
35//!     .discover([
36//!         Trial::test("succeeding_test", move |_| Ok(())),
37//!         Trial::test("failing_test", move |_| Err(RunError::fail("Woops"))),
38//!     ])
39//!     .main();
40//! ```
41//! Instead of returning `Ok` or `Err` directly, you want to actually perform
42//! your tests, of course. See [`Trial::test`] for more information on how to
43//! define a test. You can of course list all your tests manually. But in many
44//! cases it is useful to generate one test per file in a directory, for
45//! example.
46//!
47//! You can then run `cargo test --test mytest` to run it. To see the CLI
48//! arguments supported by this crate, run `cargo test --test mytest -- -h`.
49//!
50//! # Known limitations and differences to the official test harness
51//!
52//! `libtest2-mimic` aims to be fully compatible with stable, non-deprecated parts of `libtest`
53//! but there are differences for now.
54//!
55//! Some of the notable differences:
56//!
57//! - Output capture and `--no-capture`: simply not supported. The official
58//!   `libtest` uses internal `std` functions to temporarily redirect output.
59//!   `libtest-mimic` cannot use those, see also [libtest2#12](https://github.com/assert-rs/libtest2/issues/12)
60//! - `--format=json` (unstable): our schema is part of an experiment to see what should be
61//!   stabilized for `libtest`, see also [libtest2#42](https://github.com/assert-rs/libtest2/issues/42)
62
63#![cfg_attr(docsrs, feature(doc_auto_cfg))]
64//#![warn(clippy::print_stderr)]
65#![warn(clippy::print_stdout)]
66
67pub struct Harness {
68    raw: Vec<std::ffi::OsString>,
69    cases: Vec<Trial>,
70}
71
72impl Harness {
73    /// Read the process's CLI arguments
74    pub fn with_env() -> Self {
75        let raw = std::env::args_os();
76        Self::with_args(raw)
77    }
78
79    /// Manually specify CLI arguments
80    pub fn with_args(args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>) -> Self {
81        Self {
82            raw: args.into_iter().map(|a| a.into()).collect(),
83            cases: Vec::new(),
84        }
85    }
86
87    /// Enumerate all test [`Trial`]s
88    pub fn discover(mut self, cases: impl IntoIterator<Item = Trial>) -> Self {
89        self.cases.extend(cases);
90        self
91    }
92
93    /// Perform the tests and exit
94    pub fn main(self) -> ! {
95        match self.run() {
96            Ok(true) => std::process::exit(0),
97            Ok(false) => std::process::exit(libtest2_harness::ERROR_EXIT_CODE),
98            Err(err) => {
99                eprintln!("{err}");
100                std::process::exit(libtest2_harness::ERROR_EXIT_CODE)
101            }
102        }
103    }
104
105    fn run(self) -> std::io::Result<bool> {
106        let harness = libtest2_harness::Harness::new();
107        let harness = match harness.with_args(self.raw) {
108            Ok(harness) => harness,
109            Err(err) => {
110                eprintln!("{err}");
111                std::process::exit(1);
112            }
113        };
114        let harness = match harness.parse() {
115            Ok(harness) => harness,
116            Err(err) => {
117                eprintln!("{err}");
118                std::process::exit(1);
119            }
120        };
121        let harness = harness.discover(self.cases.into_iter().map(|t| TrialCase { inner: t }))?;
122        harness.run()
123    }
124}
125
126/// A test case to be run
127pub struct Trial {
128    name: String,
129    #[allow(clippy::type_complexity)]
130    runner: Box<dyn Fn(RunContext<'_>) -> Result<(), RunError> + Send + Sync>,
131}
132
133impl Trial {
134    pub fn test(
135        name: impl Into<String>,
136        runner: impl Fn(RunContext<'_>) -> Result<(), RunError> + Send + Sync + 'static,
137    ) -> Self {
138        Self {
139            name: name.into(),
140            runner: Box::new(runner),
141        }
142    }
143}
144
145struct TrialCase {
146    inner: Trial,
147}
148
149impl libtest2_harness::Case for TrialCase {
150    fn name(&self) -> &str {
151        &self.inner.name
152    }
153    fn kind(&self) -> libtest2_harness::TestKind {
154        Default::default()
155    }
156    fn source(&self) -> Option<&libtest2_harness::Source> {
157        None
158    }
159    fn exclusive(&self, _: &libtest2_harness::TestContext) -> bool {
160        false
161    }
162
163    fn run(
164        &self,
165        context: &libtest2_harness::TestContext,
166    ) -> Result<(), libtest2_harness::RunError> {
167        (self.inner.runner)(RunContext { inner: context }).map_err(|e| e.inner)
168    }
169}
170
171pub type RunResult = Result<(), RunError>;
172
173#[derive(Debug)]
174pub struct RunError {
175    inner: libtest2_harness::RunError,
176}
177
178impl RunError {
179    pub fn with_cause(cause: impl std::error::Error + Send + Sync + 'static) -> Self {
180        Self {
181            inner: libtest2_harness::RunError::with_cause(cause),
182        }
183    }
184
185    pub fn fail(cause: impl std::fmt::Display) -> Self {
186        Self {
187            inner: libtest2_harness::RunError::fail(cause),
188        }
189    }
190}
191
192pub struct RunContext<'t> {
193    inner: &'t libtest2_harness::TestContext,
194}
195
196impl<'t> RunContext<'t> {
197    /// Request this test to be ignored
198    ///
199    /// May be overridden by the CLI
200    ///
201    /// **Note:** prefer [`RunContext::ignore_for`]
202    pub fn ignore(&self) -> Result<(), RunError> {
203        self.inner.ignore().map_err(|e| RunError { inner: e })
204    }
205
206    /// Request this test to be ignored
207    ///
208    /// May be overridden by the CLI
209    pub fn ignore_for(&self, reason: impl std::fmt::Display) -> Result<(), RunError> {
210        self.inner
211            .ignore_for(reason)
212            .map_err(|e| RunError { inner: e })
213    }
214}
215
216#[doc = include_str!("../README.md")]
217#[cfg(doctest)]
218pub struct ReadmeDoctests;