libtest2_mimic/
lib.rs

1//! An experimental replacement for libtest-mimic
2//!
3//! # Usage
4//!
5//! To use this, you most likely want to add a manual `[[test]]` section to
6//! `Cargo.toml` and set `harness = false`. For example:
7//!
8//! ```toml
9//! [[test]]
10//! name = "mytest"
11//! path = "tests/mytest.rs"
12//! harness = false
13//! ```
14//!
15//! And in `tests/mytest.rs` you would call [`Harness::main`] in the `main` function:
16//!
17//! ```no_run
18//! libtest2_mimic::Harness::with_env()
19//!     .main();
20//! ```
21//!
22
23#![cfg_attr(docsrs, feature(doc_auto_cfg))]
24//#![warn(clippy::print_stderr)]
25#![warn(clippy::print_stdout)]
26
27pub struct Harness {
28    raw: Vec<std::ffi::OsString>,
29    cases: Vec<Trial>,
30}
31
32impl Harness {
33    pub fn with_args(args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>) -> Self {
34        Self {
35            raw: args.into_iter().map(|a| a.into()).collect(),
36            cases: Vec::new(),
37        }
38    }
39
40    pub fn with_env() -> Self {
41        let raw = std::env::args_os();
42        Self::with_args(raw)
43    }
44
45    pub fn discover(mut self, cases: impl IntoIterator<Item = Trial>) -> Self {
46        self.cases.extend(cases);
47        self
48    }
49
50    pub fn main(self) -> ! {
51        match self.run() {
52            Ok(true) => std::process::exit(0),
53            Ok(false) => std::process::exit(libtest2_harness::ERROR_EXIT_CODE),
54            Err(err) => {
55                eprintln!("{err}");
56                std::process::exit(libtest2_harness::ERROR_EXIT_CODE)
57            }
58        }
59    }
60
61    fn run(self) -> std::io::Result<bool> {
62        let harness = libtest2_harness::Harness::new();
63        let harness = match harness.with_args(self.raw) {
64            Ok(harness) => harness,
65            Err(err) => {
66                eprintln!("{err}");
67                std::process::exit(1);
68            }
69        };
70        let harness = match harness.parse() {
71            Ok(harness) => harness,
72            Err(err) => {
73                eprintln!("{err}");
74                std::process::exit(1);
75            }
76        };
77        let harness = harness.discover(self.cases.into_iter().map(|t| TrialCase { inner: t }))?;
78        harness.run()
79    }
80}
81
82pub struct Trial {
83    name: String,
84    #[allow(clippy::type_complexity)]
85    runner: Box<dyn Fn(RunContext<'_>) -> Result<(), RunError> + Send + Sync>,
86}
87
88impl Trial {
89    pub fn test(
90        name: impl Into<String>,
91        runner: impl Fn(RunContext<'_>) -> Result<(), RunError> + Send + Sync + 'static,
92    ) -> Self {
93        Self {
94            name: name.into(),
95            runner: Box::new(runner),
96        }
97    }
98}
99
100struct TrialCase {
101    inner: Trial,
102}
103
104impl libtest2_harness::Case for TrialCase {
105    fn name(&self) -> &str {
106        &self.inner.name
107    }
108    fn kind(&self) -> libtest2_harness::TestKind {
109        Default::default()
110    }
111    fn source(&self) -> Option<&libtest2_harness::Source> {
112        None
113    }
114    fn exclusive(&self, _: &libtest2_harness::TestContext) -> bool {
115        false
116    }
117
118    fn run(
119        &self,
120        context: &libtest2_harness::TestContext,
121    ) -> Result<(), libtest2_harness::RunError> {
122        (self.inner.runner)(RunContext { inner: context }).map_err(|e| e.inner)
123    }
124}
125
126pub type RunResult = Result<(), RunError>;
127
128#[derive(Debug)]
129pub struct RunError {
130    inner: libtest2_harness::RunError,
131}
132
133impl RunError {
134    pub fn with_cause(cause: impl std::error::Error + Send + Sync + 'static) -> Self {
135        Self {
136            inner: libtest2_harness::RunError::with_cause(cause),
137        }
138    }
139
140    pub fn fail(cause: impl std::fmt::Display) -> Self {
141        Self {
142            inner: libtest2_harness::RunError::fail(cause),
143        }
144    }
145}
146
147pub struct RunContext<'t> {
148    inner: &'t libtest2_harness::TestContext,
149}
150
151impl<'t> RunContext<'t> {
152    pub fn ignore(&self) -> Result<(), RunError> {
153        self.inner.ignore().map_err(|e| RunError { inner: e })
154    }
155
156    pub fn ignore_for(&self, reason: impl std::fmt::Display) -> Result<(), RunError> {
157        self.inner
158            .ignore_for(reason)
159            .map_err(|e| RunError { inner: e })
160    }
161}
162
163#[doc = include_str!("../README.md")]
164#[cfg(doctest)]
165pub struct ReadmeDoctests;