use std::{
env::args_os,
ffi::OsString,
fs::{read_dir, read_to_string},
panic::{catch_unwind, resume_unwind, RefUnwindSafe},
path::{Path, PathBuf},
};
use expect_test::expect_file;
const PREFIX: &str = "trycall=";
#[macro_export]
macro_rules! trycall {
[$path:expr] => {{
let crate_root_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let path = crate_root_dir.join($path);
$crate::TestCases::new(path)
}}
}
struct TestCase {
src: PathBuf,
expected: PathBuf,
}
impl TestCase {
fn with<F>(&self, f: F)
where
F: Fn(&str) -> String,
{
let src = read_to_string(&self.src)
.unwrap_or_else(|err| panic!("Failed to read file {:?}: {err}", self.src));
expect_file![&self.expected].assert_eq(&f(&src));
}
}
pub struct TestCases {
cases: Vec<TestCase>,
}
impl TestCases {
pub fn new<P>(path: P) -> Self
where
P: AsRef<Path>,
{
let path = path.as_ref();
let mut cases = Vec::new();
let metadata = path
.metadata()
.unwrap_or_else(|err| panic!("Failed to get metadata for {path:?}: {err}"));
let (filters, entries) = if metadata.is_dir() {
let filters = args_os()
.flat_map(OsString::into_string)
.filter_map(|mut arg| {
if arg.starts_with(PREFIX) && arg != PREFIX {
Some(arg.split_off(PREFIX.len()))
} else {
None
}
})
.collect();
let entries = read_dir(path)
.unwrap_or_else(|err| panic!("Failed to read dir {path:?}: {err}"))
.map(|entry| entry.unwrap().path())
.filter(|p| p.extension().unwrap() == "txt")
.collect();
(filters, entries)
} else if metadata.is_file() && path.extension().unwrap() == "txt" {
(vec![], vec![path.to_path_buf()])
} else {
panic!("Invalid path: {path:?}");
};
for path in entries {
let mut expected = path.clone();
expected.set_extension("out");
if !filters.is_empty() {
let name = path.file_stem().unwrap().to_string_lossy();
if !filters.iter().any(|filter| name.contains(filter)) {
continue;
}
}
cases.push(TestCase {
src: path,
expected,
});
}
Self { cases }
}
pub fn with<F>(&self, f: F)
where
F: Fn(&str) -> String + RefUnwindSafe,
{
let mut failed = vec![];
for case in &self.cases {
if let Err(err) = catch_unwind(|| case.with(&f)) {
failed.push((case.src.to_string_lossy(), err.downcast::<String>()));
}
}
if !failed.is_empty() {
for (src, err) in failed {
if let Ok(err) = err {
println!(
"\n\x1b[1m\x1b[91merror\x1b[97m: expect test panicked\x1b[0m\x1b[1m\x1b[34m\n -->\x1b[0m {src}\n\n{err}\n",
);
}
}
resume_unwind(Box::new(()));
}
}
}