#![doc = include_str!("../README.md")]
#![feature(test)]
extern crate test;
use std::{
borrow::Cow,
env,
path::{Path, PathBuf},
process::{Command, Stdio, Termination},
};
use test::{TestDesc, TestDescAndFn, TestFn, TestName, TestType, test_main};
#[macro_export]
macro_rules! dyntest {
($f:expr $(,)?) => {
fn main() {
$crate::_dyntest(env!("CARGO_MANIFEST_DIR"), file!(), line!() as usize, column!() as usize, $f)
}
#[test]
#[allow(unexpected_cfgs)]
fn dyntest() {
#[cfg(not(rust_analyzer))]
{ $crate::_dyntest_harness_error!() }
}
};
($($f:ident),+ $(,)?) => {
$crate::dyntest!(|tester| {
$(tester.group(stringify!($f), $f);)*
});
};
}
#[doc(hidden)]
pub fn _dyntest(
manifest: &'static str,
file: &'static str,
line: usize,
col: usize,
f: impl FnOnce(&mut DynTester),
) {
let args = env::args().collect::<Vec<_>>();
let mut tester = DynTester { manifest, file, line, col, tests: vec![], group: String::new() };
if args.get(1).is_some_and(|x| x == "dyntest") {
let bin = args[0].clone();
tester.test("dyntest", || -> Result<(), String> {
let output = Command::new(bin)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| e.to_string())?;
if output.status.success() { Ok(()) } else { Err("dyntests failed".into()) }
});
} else {
f(&mut tester);
}
let tests = tester.tests.into_iter().map(|x| x.0).collect();
test_main(&args, tests, None)
}
pub struct DynTester {
manifest: &'static str,
file: &'static str,
line: usize,
col: usize,
tests: Vec<DynTest>,
group: String,
}
impl DynTester {
pub fn test<T: Termination, F: FnOnce() -> T + Send + 'static>(
&mut self,
name: impl Into<Name>,
f: F,
) -> &mut DynTest {
let index = self.tests.len();
self.tests.push(DynTest(TestDescAndFn {
desc: TestDesc {
name: TestName::DynTestName(format!("{}{}", self.group, name.into().0)),
ignore: false,
ignore_message: None,
source_file: self.file,
start_line: self.line,
start_col: self.col,
end_line: self.line,
end_col: self.col,
should_panic: test::ShouldPanic::No,
compile_fail: false,
no_run: false,
test_type: TestType::IntegrationTest,
},
testfn: TestFn::DynTestFn(Box::new(|| test::assert_test_result(f()))),
}));
&mut self.tests[index]
}
pub fn group(&mut self, name: impl Into<Name>, f: impl FnOnce(&mut Self)) {
let len = self.group.len();
self.group.push_str(&name.into().0);
self.group.push_str("::");
f(self);
self.group.truncate(len);
}
pub fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
Path::join(self.manifest.as_ref(), path)
}
#[cfg(feature = "glob")]
pub fn glob(&self, pattern: &str) -> impl Iterator<Item = (Name, &'static Path)> {
self.glob_in(".", pattern)
}
#[cfg(feature = "glob")]
pub fn glob_in(
&self,
base: impl AsRef<Path>,
pattern: &str,
) -> impl Iterator<Item = (Name, &'static Path)> {
use globset::GlobBuilder;
use walkdir::WalkDir;
let base = self.resolve(base);
let walker = WalkDir::new(&base).follow_links(true);
let glob = GlobBuilder::new(pattern)
.case_insensitive(true)
.literal_separator(true)
.build()
.unwrap()
.compile_matcher();
walker.into_iter().filter_map(move |file| {
let file = file.unwrap();
let path = file.path();
let relative_path = path.strip_prefix(&base).ok()?;
glob.is_match(relative_path).then(|| (relative_path.into(), leak(path.to_owned())))
})
}
}
#[repr(transparent)]
pub struct DynTest(TestDescAndFn);
impl DynTest {
pub fn ignore(&mut self, ignore: impl Into<Ignore>) -> &mut Self {
let ignore = ignore.into();
self.0.desc.ignore = ignore.ignore();
self.0.desc.ignore_message = ignore.message();
self
}
pub fn should_panic(&mut self, should_panic: impl Into<ShouldPanic>) -> &mut Self {
self.0.desc.should_panic = should_panic.into().0;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Name(Cow<'static, str>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Ignore {
NoIgnore,
Ignore(Option<&'static str>),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ShouldPanic(test::ShouldPanic);
#[allow(non_snake_case, non_upper_case_globals)]
impl ShouldPanic {
pub const No: ShouldPanic = ShouldPanic(test::ShouldPanic::No);
pub const Yes: ShouldPanic = ShouldPanic(test::ShouldPanic::Yes);
pub fn YesWithMessage(message: &'static str) -> Self {
ShouldPanic(test::ShouldPanic::YesWithMessage(message))
}
}
impl Into<String> for Name {
fn into(self) -> String {
self.0.into_owned()
}
}
impl From<&'static str> for Name {
fn from(value: &'static str) -> Self {
Name(Cow::Borrowed(value))
}
}
impl From<String> for Name {
fn from(value: String) -> Self {
Name(Cow::Owned(value))
}
}
impl From<&Path> for Name {
fn from(value: &Path) -> Self {
Name(Cow::Owned(
value
.with_extension("")
.components()
.map(|x| x.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("::"),
))
}
}
impl Ignore {
pub fn ignore(&self) -> bool {
matches!(self, Ignore::Ignore(_))
}
pub fn message(&self) -> Option<&'static str> {
match *self {
Ignore::NoIgnore => None,
Ignore::Ignore(message) => message,
}
}
}
impl From<bool> for Ignore {
fn from(value: bool) -> Self {
if value { Ignore::Ignore(None) } else { Ignore::NoIgnore }
}
}
impl From<&'static str> for Ignore {
fn from(value: &'static str) -> Self {
Ignore::Ignore(Some(value))
}
}
impl From<String> for Ignore {
fn from(value: String) -> Self {
leak::<str>(value).into()
}
}
impl From<bool> for ShouldPanic {
fn from(value: bool) -> Self {
if value { ShouldPanic::Yes } else { ShouldPanic::No }
}
}
impl From<&'static str> for ShouldPanic {
fn from(value: &'static str) -> Self {
ShouldPanic::YesWithMessage(value)
}
}
impl From<String> for ShouldPanic {
fn from(value: String) -> Self {
leak::<str>(value).into()
}
}
fn leak<T: ?Sized>(value: impl Into<Box<T>>) -> &'static T {
Box::leak(value.into())
}
#[doc(hidden)]
#[macro_export]
macro_rules! _dyntest_harness_error {
() => {
compile_error!(
r#"`dyntest!` was invoked, but the default test harness is active, so this has no effect
to fix this, add `harness = false` to this test in your `Cargo.toml`:
```toml
[[test]]
name = "..."
harness = false
```
"#
);
};
}