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;