use crate::{data_source::TestEntry, DataSource, Result};
use camino::{Utf8Path, Utf8PathBuf};
use libtest_mimic::{Arguments, Trial};
use std::{path::Path, process::ExitCode};
#[doc(hidden)]
pub fn runner(requirements: &[Requirements]) -> ExitCode {
if let Some(cwd) = custom_cwd() {
std::env::set_current_dir(cwd).expect("set custom working directory");
}
let args = Arguments::from_args();
let tests = find_tests(&args, requirements);
let conclusion = libtest_mimic::run(&args, tests);
conclusion.exit_code()
}
fn custom_cwd() -> Option<Utf8PathBuf> {
std::env::var("__DATATEST_CWD").ok().map(Utf8PathBuf::from)
}
fn find_tests(args: &Arguments, requirements: &[Requirements]) -> Vec<Trial> {
let tests: Vec<_> = if let Some(exact_filter) = exact_filter(args) {
let exact_tests: Vec<_> = requirements
.iter()
.filter_map(|req| req.exact(exact_filter))
.collect();
match NextestKind::determine() {
NextestKind::InUse { process_per_test } => {
if exact_tests.is_empty() {
panic!("Failed to find exact match for filter {exact_filter}");
}
if process_per_test && exact_tests.len() > 1 {
panic!(
"Only expected one but found {} exact matches for filter {exact_filter}",
exact_tests.len()
);
}
}
NextestKind::NotInUse => {}
}
exact_tests
} else if is_full_scan_forbidden(args) {
panic!("Exact filter was expected to be used");
} else {
let mut tests: Vec<_> = requirements.iter().flat_map(|req| req.expand()).collect();
tests.sort_unstable_by(|a, b| a.name().cmp(b.name()));
tests
};
tests
}
#[derive(Clone, Copy, Debug)]
enum NextestKind {
NotInUse,
InUse { process_per_test: bool },
}
impl NextestKind {
fn determine() -> Self {
if std::env::var("NEXTEST").as_deref() == Ok("1") {
let process_per_test =
std::env::var("NEXTEST_EXECUTION_MODE").as_deref() == Ok("process-per-test");
Self::InUse { process_per_test }
} else {
Self::NotInUse
}
}
}
fn is_full_scan_forbidden(args: &Arguments) -> bool {
!args.list && std::env::var("__DATATEST_FULL_SCAN_FORBIDDEN").as_deref() == Ok("1")
}
fn exact_filter(args: &Arguments) -> Option<&str> {
if args.exact && args.skip.is_empty() {
args.filter.as_deref()
} else {
None
}
}
#[doc(hidden)]
pub struct Requirements {
test: TestFn,
test_name: String,
root: DataSource,
pattern: String,
}
impl Requirements {
#[doc(hidden)]
pub fn new(test: TestFn, test_name: String, root: DataSource, pattern: String) -> Self {
if !test.loads_data() && root.is_in_memory() {
panic!(
"test data for '{}' is stored in memory, so it \
must accept file contents as an argument",
test_name
);
}
Self {
test,
test_name,
root,
pattern,
}
}
fn trial(&self, entry: TestEntry) -> Trial {
let testfn = self.test;
let name = entry.derive_test_name(&self.test_name);
Trial::test(name, move || {
testfn
.call(entry)
.map_err(|err| format!("{:?}", err).into())
})
}
fn exact(&self, filter: &str) -> Option<Trial> {
let entry = self.root.derive_exact(filter, &self.test_name)?;
entry.exists().then(|| self.trial(entry))
}
fn expand(&self) -> Vec<Trial> {
let re = fancy_regex::Regex::new(&self.pattern)
.unwrap_or_else(|_| panic!("invalid regular expression: '{}'", self.pattern));
let tests: Vec<_> = self
.root
.walk_files()
.filter_map(|entry_res| {
let entry = entry_res.expect("error reading directory");
let path_str = entry.match_path().as_str();
if re.is_match(path_str).unwrap_or_else(|error| {
panic!(
"error matching pattern '{}' against path '{}' : {}",
self.pattern, path_str, error
)
}) {
Some(self.trial(entry))
} else {
None
}
})
.collect();
if tests.is_empty() {
panic!(
"no test cases found for test '{}' -- scanned {} with pattern '{}'",
self.test_name,
self.root.display(),
self.pattern,
);
}
tests
}
}
#[derive(Clone, Copy)]
#[doc(hidden)]
pub enum TestFn {
Base(TestFnBase),
LoadString(TestFnLoadString),
LoadBinary(TestFnLoadBinary),
}
impl TestFn {
fn loads_data(&self) -> bool {
match self {
TestFn::Base(_) => false,
TestFn::LoadString(_) | TestFn::LoadBinary(_) => true,
}
}
fn call(&self, entry: TestEntry) -> Result<()> {
match self {
TestFn::Base(f) => {
let path = entry
.disk_path()
.expect("test entry being on disk was checked in the constructor");
f.call(path)
}
TestFn::LoadString(f) => f.call(entry),
TestFn::LoadBinary(f) => f.call(entry),
}
}
}
#[derive(Clone, Copy)]
#[doc(hidden)]
pub enum TestFnBase {
Path(fn(&Path) -> Result<()>),
Utf8Path(fn(&Utf8Path) -> Result<()>),
}
impl TestFnBase {
fn call(&self, path: &Utf8Path) -> Result<()> {
match self {
TestFnBase::Path(f) => f(path.as_ref()),
TestFnBase::Utf8Path(f) => f(path),
}
}
}
#[derive(Clone, Copy)]
#[doc(hidden)]
pub enum TestFnLoadString {
Path(fn(&Path, String) -> Result<()>),
Utf8Path(fn(&Utf8Path, String) -> Result<()>),
}
impl TestFnLoadString {
fn call(&self, entry: TestEntry) -> Result<()> {
let contents = entry.read_as_string()?;
match self {
TestFnLoadString::Path(f) => f(entry.test_path().as_ref(), contents),
TestFnLoadString::Utf8Path(f) => f(entry.test_path(), contents),
}
}
}
#[derive(Clone, Copy)]
#[doc(hidden)]
pub enum TestFnLoadBinary {
Path(fn(&Path, Vec<u8>) -> Result<()>),
Utf8Path(fn(&Utf8Path, Vec<u8>) -> Result<()>),
}
impl TestFnLoadBinary {
fn call(&self, entry: TestEntry) -> Result<()> {
let contents = entry.read()?;
match self {
TestFnLoadBinary::Path(f) => f(entry.test_path().as_ref(), contents),
TestFnLoadBinary::Utf8Path(f) => f(entry.test_path(), contents),
}
}
}
#[doc(hidden)]
pub mod test_kinds {
use super::*;
mod private {
pub trait PathSealed {}
pub trait Utf8PathSealed {}
pub trait PathStringSealed {}
pub trait Utf8PathStringSealed {}
pub trait PathBytesSealed {}
pub trait Utf8PathBytesSealed {}
}
#[doc(hidden)]
pub struct PathTag;
impl PathTag {
#[inline]
pub fn resolve(self, f: fn(&Path) -> Result<()>) -> TestFn {
TestFn::Base(TestFnBase::Path(f))
}
}
#[doc(hidden)]
pub trait PathKind: private::PathSealed {
#[inline]
fn kind(&self) -> PathTag {
PathTag
}
}
impl<F: Fn(&Path) -> Result<()>> private::PathSealed for F {}
impl<F: Fn(&Path) -> Result<()>> PathKind for F {}
#[doc(hidden)]
pub struct Utf8PathTag;
impl Utf8PathTag {
#[inline]
pub fn resolve(&self, f: fn(&Utf8Path) -> Result<()>) -> TestFn {
TestFn::Base(TestFnBase::Utf8Path(f))
}
}
#[doc(hidden)]
pub trait Utf8PathKind: private::Utf8PathSealed {
#[inline]
fn kind(&self) -> Utf8PathTag {
Utf8PathTag
}
}
impl<F: Fn(&Utf8Path) -> Result<()>> private::Utf8PathSealed for F {}
impl<F: Fn(&Utf8Path) -> Result<()>> Utf8PathKind for F {}
#[doc(hidden)]
pub struct PathStringTag;
impl PathStringTag {
#[inline]
pub fn resolve(self, f: fn(&Path, String) -> Result<()>) -> TestFn {
TestFn::LoadString(TestFnLoadString::Path(f))
}
}
#[doc(hidden)]
pub trait PathStringKind: private::PathStringSealed {
#[inline]
fn kind(&self) -> PathStringTag {
PathStringTag
}
}
impl<F: Fn(&Path, String) -> Result<()>> private::PathStringSealed for F {}
impl<F: Fn(&Path, String) -> Result<()>> PathStringKind for F {}
#[doc(hidden)]
pub struct Utf8PathStringTag;
impl Utf8PathStringTag {
#[inline]
pub fn resolve(self, f: fn(&Utf8Path, String) -> Result<()>) -> TestFn {
TestFn::LoadString(TestFnLoadString::Utf8Path(f))
}
}
#[doc(hidden)]
pub trait Utf8PathStringKind: private::Utf8PathStringSealed {
#[inline]
fn kind(&self) -> Utf8PathStringTag {
Utf8PathStringTag
}
}
impl<F: Fn(&Utf8Path, String) -> Result<()>> private::Utf8PathStringSealed for F {}
impl<F: Fn(&Utf8Path, String) -> Result<()>> Utf8PathStringKind for F {}
#[doc(hidden)]
pub struct PathBytesTag;
impl PathBytesTag {
#[inline]
pub fn resolve(self, f: fn(&Path, Vec<u8>) -> Result<()>) -> TestFn {
TestFn::LoadBinary(TestFnLoadBinary::Path(f))
}
}
#[doc(hidden)]
pub trait PathBytesKind: private::PathBytesSealed {
#[inline]
fn kind(&self) -> PathBytesTag {
PathBytesTag
}
}
impl<F: Fn(&Path, Vec<u8>) -> Result<()>> private::PathBytesSealed for F {}
impl<F: Fn(&Path, Vec<u8>) -> Result<()>> PathBytesKind for F {}
#[doc(hidden)]
pub struct Utf8PathBytesTag;
impl Utf8PathBytesTag {
#[inline]
pub fn resolve(self, f: fn(&Utf8Path, Vec<u8>) -> Result<()>) -> TestFn {
TestFn::LoadBinary(TestFnLoadBinary::Utf8Path(f))
}
}
#[doc(hidden)]
pub trait Utf8PathBytesKind: private::Utf8PathBytesSealed {
#[inline]
fn kind(&self) -> Utf8PathBytesTag {
Utf8PathBytesTag
}
}
impl<F: Fn(&Utf8Path, Vec<u8>) -> Result<()>> private::Utf8PathBytesSealed for F {}
impl<F: Fn(&Utf8Path, Vec<u8>) -> Result<()>> Utf8PathBytesKind for F {}
}
#[cfg(all(test, feature = "include-dir"))]
mod include_dir_tests {
use super::*;
use std::borrow::Cow;
#[test]
#[should_panic = "test data for 'my_test' is stored in memory, \
so it must accept file contents as an argument"]
fn include_dir_without_arg() {
fn my_test(_: &Path) -> Result<()> {
Ok(())
}
Requirements::new(
TestFn::Base(TestFnBase::Path(my_test)),
"my_test".to_owned(),
DataSource::IncludeDir(Cow::Owned(include_dir::include_dir!("tests/files"))),
"xxx".to_owned(),
);
}
}