blob: e5b2b6cba89bc546458ae5d19222fbc7ecf49a0b [file] [log] [blame]
//! [`Harness`] for discovering test inputs and asserting against snapshot files
//!
//! # Examples
//!
//! ```rust,no_run
//! snapbox::harness::Harness::new(
//! "tests/fixtures/invalid",
//! setup,
//! test,
//! )
//! .select(["tests/cases/*.in"])
//! .action_env("SNAPSHOTS")
//! .test();
//!
//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case {
//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned();
//! let expected = input_path.with_extension("out");
//! snapbox::harness::Case {
//! name,
//! fixture: input_path,
//! expected,
//! }
//! }
//!
//! fn test(input_path: &std::path::Path) -> Result<usize, Box<std::error::Error>> {
//! let raw = std::fs::read_to_string(input_path)?;
//! let num = raw.parse::<usize>()?;
//!
//! let actual = num + 10;
//!
//! Ok(actual)
//! }
//! ```
use crate::Action;
pub struct Harness<S, T> {
root: std::path::PathBuf,
overrides: Option<ignore::overrides::Override>,
setup: S,
test: T,
action: Action,
}
impl<S, T, I, E> Harness<S, T>
where
I: std::fmt::Display,
E: std::fmt::Display,
S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static,
T: Fn(&std::path::Path) -> Result<I, E> + Send + Sync + 'static,
{
pub fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self {
Self {
root: root.into(),
overrides: None,
setup,
test,
action: Action::Verify,
}
}
/// Path patterns for selecting input files
///
/// This used gitignore syntax
pub fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self {
let mut overrides = ignore::overrides::OverrideBuilder::new(&self.root);
for line in patterns {
overrides.add(line).unwrap();
}
self.overrides = Some(overrides.build().unwrap());
self
}
/// Read the failure action from an environment variable
pub fn action_env(mut self, var_name: &str) -> Self {
let action = Action::with_env_var(var_name);
self.action = action.unwrap_or(self.action);
self
}
/// Override the failure action
pub fn action(mut self, action: Action) -> Self {
self.action = action;
self
}
/// Run tests
pub fn test(self) -> ! {
let mut walk = ignore::WalkBuilder::new(&self.root);
walk.standard_filters(false);
let tests = walk.build().filter_map(|entry| {
let entry = entry.unwrap();
let is_dir = entry.file_type().map(|f| f.is_dir()).unwrap_or(false);
let path = entry.into_path();
if let Some(overrides) = &self.overrides {
overrides
.matched(&path, is_dir)
.is_whitelist()
.then(|| path)
} else {
Some(path)
}
});
let tests: Vec<_> = tests
.into_iter()
.map(|path| {
let case = (self.setup)(path);
Test {
name: case.name.clone(),
kind: "".into(),
is_ignored: false,
is_bench: false,
data: case,
}
})
.collect();
let args = libtest_mimic::Arguments::from_args();
libtest_mimic::run_tests(&args, tests, move |test| {
match (self.test)(&test.data.fixture) {
Ok(actual) => {
let actual = actual.to_string();
let actual = crate::Data::text(actual).map_text(crate::utils::normalize_lines);
let verify = Verifier::new()
.palette(crate::report::Palette::auto())
.action(self.action);
verify.verify(&test.data.expected, actual)
}
Err(err) => libtest_mimic::Outcome::Failed {
msg: Some(err.to_string()),
},
}
})
.exit()
}
}
struct Verifier {
palette: crate::report::Palette,
action: Action,
}
impl Verifier {
fn new() -> Self {
Default::default()
}
fn palette(mut self, palette: crate::report::Palette) -> Self {
self.palette = palette;
self
}
fn action(mut self, action: Action) -> Self {
self.action = action;
self
}
fn verify(
&self,
expected_path: &std::path::Path,
actual: crate::Data,
) -> libtest_mimic::Outcome {
match self.action {
Action::Skip => libtest_mimic::Outcome::Ignored,
Action::Ignore => {
let _ = self.do_verify(expected_path, actual);
libtest_mimic::Outcome::Ignored
}
Action::Verify => self.do_verify(expected_path, actual),
Action::Overwrite => self.do_overwrite(expected_path, actual),
}
}
fn do_overwrite(
&self,
expected_path: &std::path::Path,
actual: crate::Data,
) -> libtest_mimic::Outcome {
match self.try_overwrite(expected_path, actual) {
Ok(()) => libtest_mimic::Outcome::Passed,
Err(err) => libtest_mimic::Outcome::Failed {
msg: Some(err.to_string()),
},
}
}
fn try_overwrite(
&self,
expected_path: &std::path::Path,
actual: crate::Data,
) -> crate::Result<()> {
actual.write_to(expected_path)?;
Ok(())
}
fn do_verify(
&self,
expected_path: &std::path::Path,
actual: crate::Data,
) -> libtest_mimic::Outcome {
match self.try_verify(expected_path, actual) {
Ok(()) => libtest_mimic::Outcome::Passed,
Err(err) => libtest_mimic::Outcome::Failed {
msg: Some(err.to_string()),
},
}
}
fn try_verify(
&self,
expected_path: &std::path::Path,
actual: crate::Data,
) -> crate::Result<()> {
let expected = crate::Data::read_from(expected_path, Some(false))?
.map_text(crate::utils::normalize_lines);
if expected != actual {
let mut buf = String::new();
crate::report::write_diff(
&mut buf,
&expected,
&actual,
Some(&expected_path.display()),
None,
self.palette,
)
.map_err(|e| e.to_string())?;
Err(buf.into())
} else {
Ok(())
}
}
}
impl Default for Verifier {
fn default() -> Self {
Self {
palette: crate::report::Palette::auto(),
action: Action::Verify,
}
}
}
pub struct Case {
pub name: String,
pub fixture: std::path::PathBuf,
pub expected: std::path::PathBuf,
}
type Test = libtest_mimic::Test<Case>;