use std::{
collections::HashMap,
io,
path::{Component, Path, PathBuf},
str,
};
use log::trace;
use thiserror::Error;
use walkdir::WalkDir;
use crate::{PrepareTestError, SnapshotMode, TemplateTestCollection, Test, TestCollectionError};
pub fn collect_tests(
test_dir: &Path,
snapshot_mode: SnapshotMode,
) -> Result<TemplateTests, CollectTestsError> {
let walk_dir = WalkDir::new(test_dir);
let entries = walk_dir
.into_iter()
.filter_entry(|entry| {
let name = entry.file_name().to_string_lossy();
if entry.file_type().is_file() && !name.ends_with("tests.toml") {
trace!("File '{name}' does not end with 'tests.toml', skipping...");
return false;
}
true
})
.filter_map(|e| e.ok());
let mut tests = vec![];
for entry in entries {
if !entry.file_type().is_file() {
continue;
}
let collection_path = entry.path();
trace!("Reading test collection at {collection_path:?}");
let root = collection_path.parent().ok_or(CollectTestsError::Io {
file_path: collection_path.into(),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"A test collection file needs to have a parent as it is inside the test directory.",
),
})?;
let test_path_components: Vec<String> = root
.strip_prefix(test_dir)
.ok()
.map(|path| {
path.components()
.filter_map(|comp| match comp {
Component::Normal(s) => s.to_str().map(|s| s.to_string()),
_ => None,
})
.collect::<Vec<String>>()
})
.unwrap_or_default();
let tests_collection = TemplateTestCollection::read_from(collection_path)?;
for test_case in tests_collection.tests {
tests.push(Test::new(
test_case,
tests_collection.name.clone(),
&test_path_components,
collection_path,
snapshot_mode,
root,
)?);
}
}
tests.sort_by(|a, b| a.descriptor.cmp(&b.descriptor));
let warnings = duplication_warnings(&tests);
Ok(TemplateTests { tests, warnings })
}
fn duplication_warnings(tests: &Vec<Test>) -> Vec<String> {
let mut groups: HashMap<&str, Vec<&Test>> = HashMap::new();
for test in tests {
groups.entry(&test.descriptor).or_default().push(test);
}
groups
.iter()
.filter_map(|(description, tests)| {
let count = tests.len();
if count < 2 {
None
} else {
Some(format!(
"There is {count} tests with the full name '{description}'."
))
}
})
.collect()
}
#[derive(Default, Debug)]
pub struct TemplateTests {
pub tests: Vec<Test>,
pub warnings: Vec<String>,
}
#[derive(Debug, Error)]
pub enum CollectTestsError {
#[error("Failed to read file '{file_path}': {source}")]
Io {
file_path: PathBuf,
#[source]
source: io::Error,
},
#[error("Issue with a test collection")]
TestCollection(#[from] TestCollectionError),
#[error("Issue with a test collection")]
PrepareTestError(#[from] PrepareTestError),
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::Path;
use tempfile::tempdir;
use crate::collect::TemplateTests;
use super::collect_tests;
#[test]
fn collect_all_tests() {
let tests_dir = tempdir().unwrap();
let base_collection = tests_dir.path().join("tests.toml");
let first_collection = tests_dir.path().join("bar.tests.toml");
let second_collection = tests_dir.path().join("dir").join("ignored.tests.toml");
write_collection(&base_collection, None);
write_collection(&first_collection, None);
write_collection(&second_collection, Some("name".to_owned()));
let TemplateTests { tests, warnings } =
collect_tests(tests_dir.path(), crate::SnapshotMode::Compare)
.expect("Failed to collect tests");
assert!(warnings.is_empty());
assert_eq!(tests.len(), 6);
assert!(tests
.iter()
.any(|test| test.name == "foo" && test.descriptor == "bar > foo"));
assert!(tests
.iter()
.any(|test| test.name == "bar" && test.descriptor == "bar > bar"));
assert!(tests
.iter()
.any(|test| test.name == "foo" && test.descriptor == "foo"));
assert!(tests
.iter()
.any(|test| test.name == "bar" && test.descriptor == "bar"));
assert!(tests
.iter()
.any(|test| test.name == "foo" && test.descriptor == "dir > name > foo"));
assert!(tests
.iter()
.any(|test| test.name == "bar" && test.descriptor == "dir > name > bar"));
}
#[test]
fn warn_for_duplicate_tests() {
let tests_dir = tempdir().unwrap();
let base_collection = tests_dir.path().join("bar.tests.toml");
let second_collection = tests_dir.path().join("bar").join("tests.toml");
write_collection(&base_collection, None);
write_collection(&second_collection, None);
let TemplateTests { tests, warnings } =
collect_tests(tests_dir.path(), crate::SnapshotMode::Compare)
.expect("Failed to collect tests");
assert_eq!(warnings.len(), 2);
assert_eq!(tests.len(), 4);
assert!(warnings.contains(&"There is 2 tests with the full name 'bar > bar'.".to_owned()));
assert!(warnings.contains(&"There is 2 tests with the full name 'bar > foo'.".to_owned()));
}
fn write_collection(path: &Path, name: Option<String>) {
_ = create_dir_all(path.parent().unwrap());
let mut file = File::create(path).unwrap();
write!(
&mut file,
r#"
{}
tests_version = 1
[[test]]
name = "foo"
[[test]]
name = "bar"
snapshot = "foo.png" # use same snapshot as the test above
"#,
name.map(|name| format!("name = \"{name}\""))
.unwrap_or_default()
)
.unwrap();
let _ = File::create(path.parent().unwrap().join("foo.png")).unwrap();
}
}