use crate::testing::test_context::TestSuite;
use std::fs;
use std::path::{Path, PathBuf};
const TEST_FILE_PATTERNS: &[&str] = &[
"*.test.js",
"*.test.ts",
"*.test.mjs",
"*.spec.js",
"*.spec.ts",
"*.spec.mjs",
];
#[derive(Debug, Clone)]
pub struct TestDiscovererConfig {
pub root_path: PathBuf,
pub test_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
}
impl Default for TestDiscovererConfig {
fn default() -> Self {
TestDiscovererConfig {
root_path: PathBuf::from("."),
test_patterns: TEST_FILE_PATTERNS.iter().map(|s| s.to_string()).collect(),
exclude_patterns: vec!["node_modules".to_string()],
}
}
}
#[derive(Debug, Clone)]
pub struct DiscoveryResult {
pub test_files: Vec<PathBuf>,
pub total_files: usize,
}
impl DiscoveryResult {
pub fn new() -> Self {
DiscoveryResult {
test_files: Vec::new(),
total_files: 0,
}
}
pub fn add_file(&mut self, path: PathBuf) {
self.test_files.push(path);
}
}
pub struct TestDiscoverer {
pub config: TestDiscovererConfig,
}
impl TestDiscoverer {
pub fn new(config: TestDiscovererConfig) -> Self {
TestDiscoverer { config }
}
pub fn discover(&self) -> std::io::Result<DiscoveryResult> {
self.discover_with_read_permission(|_| Ok(()))
}
pub fn discover_with_read_permission<F>(
&self,
mut check_read: F,
) -> std::io::Result<DiscoveryResult>
where
F: FnMut(&Path) -> std::io::Result<()>,
{
let mut result = DiscoveryResult::new();
self.discover_recursive(&self.config.root_path, &mut result, &mut check_read)?;
Ok(result)
}
fn discover_recursive<F>(
&self,
path: &Path,
result: &mut DiscoveryResult,
check_read: &mut F,
) -> std::io::Result<()>
where
F: FnMut(&Path) -> std::io::Result<()>,
{
check_read(path)?;
let entries: _ = fs::read_dir(path)?;
for entry in entries {
let entry: _ = entry?;
let path: _ = entry.path();
if self.should_exclude(&path) {
continue;
}
if path.is_dir() {
self.discover_recursive(&path, result, check_read)?;
} else if path.is_file() {
if self.is_test_file(&path) {
result.add_file(path);
}
}
}
Ok(())
}
fn should_exclude(&self, path: &Path) -> bool {
let path_str: _ = path.to_string_lossy();
for pattern in &self.config.exclude_patterns {
if path_str.contains(pattern) {
return true;
}
}
false
}
fn is_test_file(&self, path: &Path) -> bool {
let file_name: _ = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
for pattern in &self.config.test_patterns {
let pattern_str = pattern.trim_start_matches('*').to_string();
if file_name.ends_with(&pattern_str) {
return true;
}
}
false
}
pub fn load_test_file(&self, path: &Path) -> std::io::Result<Vec<TestSuite>> {
let result = std::fs::read_to_string(path);
let _code = result.map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to read test file: {}", e))
})?;
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let suite = TestSuite {
name: format!("Test Suite - {}", file_name),
parent: None,
child_suites: Vec::new(),
tests: Vec::new(),
before_each: Vec::new(),
after_each: Vec::new(),
before_all: None,
after_all: None,
};
Ok(vec![suite])
}
pub fn load_all_tests(&self, discovery: &DiscoveryResult) -> std::io::Result<Vec<TestSuite>> {
let mut all_suites = Vec::new();
for test_file in &discovery.test_files {
match self.load_test_file(test_file) {
Ok(suites) => all_suites.extend(suites),
Err(e) => eprintln!("Warning: Failed to load test file {:?}: {}", test_file, e),
}
}
Ok(all_suites)
}
}