1#![doc = include_str!("../README.md")]
2#![feature(test)]
3
4extern crate test;
5
6use std::{
7 borrow::Cow,
8 env,
9 path::{Path, PathBuf},
10 process::{Command, Stdio, Termination},
11};
12use test::{TestDesc, TestDescAndFn, TestFn, TestName, TestType, test_main};
13
14#[macro_export]
17macro_rules! dyntest {
18 ($f:expr $(,)?) => {
19 fn main() {
20 $crate::_dyntest(env!("CARGO_MANIFEST_DIR"), file!(), line!() as usize, column!() as usize, $f)
21 }
22
23 #[test]
24 #[allow(unexpected_cfgs)]
25 fn dyntest() {
26 #[cfg(not(rust_analyzer))]
27 { $crate::_dyntest_harness_error!() }
28 }
29 };
30 ($($f:ident),+ $(,)?) => {
31 $crate::dyntest!(|tester| {
32 $(tester.group(stringify!($f), $f);)*
33 });
34 };
35}
36
37#[doc(hidden)]
38pub fn _dyntest(
39 manifest: &'static str,
40 file: &'static str,
41 line: usize,
42 col: usize,
43 f: impl FnOnce(&mut DynTester),
44) {
45 let args = env::args().collect::<Vec<_>>();
46 let mut tester = DynTester { manifest, file, line, col, tests: vec![], group: String::new() };
47 if args.get(1).is_some_and(|x| x == "dyntest") {
48 let bin = args[0].clone();
50 tester.test("dyntest", || -> Result<(), String> {
51 let output = Command::new(bin)
52 .stdout(Stdio::inherit())
53 .stderr(Stdio::inherit())
54 .output()
55 .map_err(|e| e.to_string())?;
56 if output.status.success() { Ok(()) } else { Err("dyntests failed".into()) }
57 });
58 } else {
59 f(&mut tester);
60 }
61 let tests = tester.tests.into_iter().map(|x| x.0).collect();
62 test_main(&args, tests, None)
63}
64
65pub struct DynTester {
67 manifest: &'static str,
68 file: &'static str,
69 line: usize,
70 col: usize,
71 tests: Vec<DynTest>,
72 group: String,
73}
74
75impl DynTester {
76 pub fn test<T: Termination, F: FnOnce() -> T + Send + 'static>(
79 &mut self,
80 name: impl Into<Name>,
81 f: F,
82 ) -> &mut DynTest {
83 let index = self.tests.len();
84 self.tests.push(DynTest(TestDescAndFn {
85 desc: TestDesc {
86 name: TestName::DynTestName(format!("{}{}", self.group, name.into().0)),
87 ignore: false,
88 ignore_message: None,
89 source_file: self.file,
90 start_line: self.line,
91 start_col: self.col,
92 end_line: self.line,
93 end_col: self.col,
94 should_panic: test::ShouldPanic::No,
95 compile_fail: false,
96 no_run: false,
97 test_type: TestType::IntegrationTest,
98 },
99 testfn: TestFn::DynTestFn(Box::new(|| test::assert_test_result(f()))),
100 }));
101 &mut self.tests[index]
102 }
103
104 pub fn group(&mut self, name: impl Into<Name>, f: impl FnOnce(&mut Self)) {
106 let len = self.group.len();
107 self.group.push_str(&name.into().0);
108 self.group.push_str("::");
109 f(self);
110 self.group.truncate(len);
111 }
112
113 pub fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
115 Path::join(self.manifest.as_ref(), path)
116 }
117
118 #[cfg(feature = "glob")]
122 pub fn glob(&self, pattern: &str) -> impl Iterator<Item = (Name, &'static Path)> {
123 self.glob_in(".", pattern)
124 }
125
126 #[cfg(feature = "glob")]
131 pub fn glob_in(
132 &self,
133 base: impl AsRef<Path>,
134 pattern: &str,
135 ) -> impl Iterator<Item = (Name, &'static Path)> {
136 use globset::GlobBuilder;
137 use walkdir::WalkDir;
138
139 let base = self.resolve(base);
140
141 let walker = WalkDir::new(&base).follow_links(true);
142
143 let glob = GlobBuilder::new(pattern)
144 .case_insensitive(true)
145 .literal_separator(true)
146 .build()
147 .unwrap()
148 .compile_matcher();
149
150 walker.into_iter().filter_map(move |file| {
151 let file = file.unwrap();
152 let path = file.path();
153 let relative_path = path.strip_prefix(&base).ok()?;
154 glob.is_match(relative_path).then(|| (relative_path.into(), leak(path.to_owned())))
155 })
156 }
157}
158
159#[repr(transparent)]
161pub struct DynTest(TestDescAndFn);
162
163impl DynTest {
164 pub fn ignore(&mut self, ignore: impl Into<Ignore>) -> &mut Self {
173 let ignore = ignore.into();
174 self.0.desc.ignore = ignore.ignore();
175 self.0.desc.ignore_message = ignore.message();
176 self
177 }
178
179 pub fn should_panic(&mut self, should_panic: impl Into<ShouldPanic>) -> &mut Self {
188 self.0.desc.should_panic = should_panic.into().0;
189 self
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
195pub struct Name(Cow<'static, str>);
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
199pub enum Ignore {
200 NoIgnore,
201 Ignore(Option<&'static str>),
202}
203
204#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
206pub struct ShouldPanic(test::ShouldPanic);
207
208#[allow(non_snake_case, non_upper_case_globals)]
209impl ShouldPanic {
210 pub const No: ShouldPanic = ShouldPanic(test::ShouldPanic::No);
211 pub const Yes: ShouldPanic = ShouldPanic(test::ShouldPanic::Yes);
212 pub fn YesWithMessage(message: &'static str) -> Self {
213 ShouldPanic(test::ShouldPanic::YesWithMessage(message))
214 }
215}
216
217impl Into<String> for Name {
218 fn into(self) -> String {
219 self.0.into_owned()
220 }
221}
222
223impl From<&'static str> for Name {
224 fn from(value: &'static str) -> Self {
225 Name(Cow::Borrowed(value))
226 }
227}
228
229impl From<String> for Name {
230 fn from(value: String) -> Self {
231 Name(Cow::Owned(value))
232 }
233}
234
235impl From<&Path> for Name {
236 fn from(value: &Path) -> Self {
237 Name(Cow::Owned(
238 value
239 .with_extension("")
240 .components()
241 .map(|x| x.as_os_str().to_string_lossy())
242 .collect::<Vec<_>>()
243 .join("::"),
244 ))
245 }
246}
247
248impl Ignore {
249 pub fn ignore(&self) -> bool {
250 matches!(self, Ignore::Ignore(_))
251 }
252 pub fn message(&self) -> Option<&'static str> {
253 match *self {
254 Ignore::NoIgnore => None,
255 Ignore::Ignore(message) => message,
256 }
257 }
258}
259
260impl From<bool> for Ignore {
261 fn from(value: bool) -> Self {
262 if value { Ignore::Ignore(None) } else { Ignore::NoIgnore }
263 }
264}
265
266impl From<&'static str> for Ignore {
267 fn from(value: &'static str) -> Self {
268 Ignore::Ignore(Some(value))
269 }
270}
271
272impl From<String> for Ignore {
273 fn from(value: String) -> Self {
274 leak::<str>(value).into()
275 }
276}
277
278impl From<bool> for ShouldPanic {
279 fn from(value: bool) -> Self {
280 if value { ShouldPanic::Yes } else { ShouldPanic::No }
281 }
282}
283
284impl From<&'static str> for ShouldPanic {
285 fn from(value: &'static str) -> Self {
286 ShouldPanic::YesWithMessage(value)
287 }
288}
289
290impl From<String> for ShouldPanic {
291 fn from(value: String) -> Self {
292 leak::<str>(value).into()
293 }
294}
295
296fn leak<T: ?Sized>(value: impl Into<Box<T>>) -> &'static T {
297 Box::leak(value.into())
298}
299
300#[doc(hidden)]
301#[macro_export]
302macro_rules! _dyntest_harness_error {
303 () => {
304 compile_error!(
305 r#"`dyntest!` was invoked, but the default test harness is active, so this has no effect
306
307to fix this, add `harness = false` to this test in your `Cargo.toml`:
308```toml
309[[test]]
310name = "..."
311harness = false
312```
313"#
314 );
315 };
316}