use crate::cli::TestFilterArgs;
use crate::discovery::{TestFunction, TestTarget, TestType};
use regex::Regex;
pub struct TestFilter<'a> {
args: &'a TestFilterArgs,
}
impl<'a> TestFilter<'a> {
pub fn new(args: &'a TestFilterArgs) -> Self {
Self { args }
}
pub fn filter_functions(&self, functions: Vec<TestFunction>) -> Vec<TestFunction> {
functions
.into_iter()
.filter(|func| self.matches_function(func))
.collect()
}
fn matches_function(&self, func: &TestFunction) -> bool {
if self.args.integration && func.test_type != TestType::Integration {
return false;
}
if self.args.unit && func.test_type != TestType::Unit {
return false;
}
if !self.args.tag.is_empty() {
let has_any_tag = self.args.tag.iter().any(|filter_tag| {
func.tags.iter().any(|test_tag| test_tag == filter_tag)
});
if !has_any_tag {
return false;
}
}
if !self.args.exclude_tag.is_empty() {
let has_excluded_tag = self.args.exclude_tag.iter().any(|filter_tag| {
func.tags.iter().any(|test_tag| test_tag == filter_tag)
});
if has_excluded_tag {
return false;
}
}
if let Some(ref name_pattern) = self.args.name {
if let Ok(re) = Regex::new(&format!(".*{}.*", regex::escape(name_pattern))) {
if !re.is_match(&func.name) {
return false;
}
}
}
if let Some(ref path_pattern) = self.args.path {
let path_str = func.file_path.to_string_lossy();
if let Ok(re) = Regex::new(&format!(".*{}.*", regex::escape(path_pattern))) {
if !re.is_match(&path_str) {
return false;
}
}
}
true
}
pub fn filter_tests(&self, tests: Vec<TestTarget>) -> Vec<TestTarget> {
tests
.into_iter()
.filter(|test| self.matches_test(test))
.collect()
}
fn matches_test(&self, test: &TestTarget) -> bool {
if self.args.integration && test.test_type != TestType::Integration {
return false;
}
if self.args.unit && test.test_type != TestType::Unit {
return false;
}
if !self.args.tag.is_empty() {
let has_any_tag = self.args.tag.iter().any(|filter_tag| {
test.tags.iter().any(|test_tag| test_tag == filter_tag)
});
if !has_any_tag {
return false;
}
}
if !self.args.exclude_tag.is_empty() {
let has_excluded_tag = self.args.exclude_tag.iter().any(|filter_tag| {
test.tags.iter().any(|test_tag| test_tag == filter_tag)
});
if has_excluded_tag {
return false;
}
}
if let Some(ref name_pattern) = self.args.name {
if let Ok(re) = Regex::new(&format!(".*{}.*", regex::escape(name_pattern))) {
if !re.is_match(&test.name) {
return false;
}
}
}
if let Some(ref path_pattern) = self.args.path {
let path_str = test.path.to_string_lossy();
if let Ok(re) = Regex::new(&format!(".*{}.*", regex::escape(path_pattern))) {
if !re.is_match(&path_str) {
return false;
}
}
}
true
}
pub fn get_filter_summary(&self) -> String {
let mut parts = Vec::new();
if self.args.integration {
parts.push("integration tests only".to_string());
}
if self.args.unit {
parts.push("unit tests only".to_string());
}
if !self.args.tag.is_empty() {
parts.push(format!("tags: {}", self.args.tag.join(", ")));
}
if !self.args.exclude_tag.is_empty() {
parts.push(format!("excluding tags: {}", self.args.exclude_tag.join(", ")));
}
if let Some(ref name) = self.args.name {
parts.push(format!("name contains: {}", name));
}
if let Some(ref path) = self.args.path {
parts.push(format!("path contains: {}", path));
}
if parts.is_empty() {
"all tests".to_string()
} else {
parts.join(", ")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_args() -> TestFilterArgs {
TestFilterArgs {
integration: false,
unit: false,
tag: Vec::new(),
exclude_tag: Vec::new(),
name: None,
path: None,
timeout: None,
list: false,
verbose: false,
test_args: Vec::new(),
}
}
fn create_test_target(name: &str, test_type: TestType, tags: Vec<String>) -> TestTarget {
TestTarget {
name: name.to_string(),
path: PathBuf::from(format!("{}.rs", name)),
test_type,
tags,
}
}
fn create_test_function(name: &str, target_name: &str, test_type: TestType, tags: Vec<String>) -> TestFunction {
TestFunction {
name: name.to_string(),
file_path: PathBuf::from(format!("tests/{}.rs", target_name)),
target_name: target_name.to_string(),
test_type,
tags,
}
}
#[test]
fn test_filter_integration_only() {
let mut args = create_test_args();
args.integration = true;
let filter = TestFilter::new(&args);
let tests = vec![
create_test_target("unit_test", TestType::Unit, vec![]),
create_test_target("integration_test", TestType::Integration, vec![]),
];
let filtered = filter.filter_tests(tests);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "integration_test");
}
#[test]
fn test_filter_by_tag() {
let mut args = create_test_args();
args.tag.push("fast".to_string());
let filter = TestFilter::new(&args);
let tests = vec![
create_test_target("test1", TestType::Unit, vec!["fast".to_string()]),
create_test_target("test2", TestType::Unit, vec!["slow".to_string()]),
];
let filtered = filter.filter_tests(tests);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "test1");
}
#[test]
fn test_exclude_tag() {
let mut args = create_test_args();
args.exclude_tag.push("slow".to_string());
let filter = TestFilter::new(&args);
let tests = vec![
create_test_target("test1", TestType::Unit, vec!["fast".to_string()]),
create_test_target("test2", TestType::Unit, vec!["slow".to_string()]),
];
let filtered = filter.filter_tests(tests);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "test1");
}
#[test]
fn test_filter_functions_by_tag() {
let mut args = create_test_args();
args.tag.push("fast".to_string());
let filter = TestFilter::new(&args);
let functions = vec![
create_test_function("test_fast_api", "api_test", TestType::Integration, vec!["fast".to_string(), "api".to_string()]),
create_test_function("test_slow_db", "db_test", TestType::Integration, vec!["slow".to_string(), "database".to_string()]),
create_test_function("test_fast_db", "db_test", TestType::Integration, vec!["fast".to_string(), "database".to_string()]),
];
let filtered = filter.filter_functions(functions);
assert_eq!(filtered.len(), 2);
assert!(filtered.iter().all(|f| f.tags.contains(&"fast".to_string())));
}
#[test]
fn test_filter_functions_exclude_tag() {
let mut args = create_test_args();
args.exclude_tag.push("slow".to_string());
let filter = TestFilter::new(&args);
let functions = vec![
create_test_function("test_fast_api", "api_test", TestType::Integration, vec!["fast".to_string()]),
create_test_function("test_slow_db", "db_test", TestType::Integration, vec!["slow".to_string()]),
];
let filtered = filter.filter_functions(functions);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "test_fast_api");
}
#[test]
fn test_filter_functions_by_name() {
let mut args = create_test_args();
args.name = Some("api".to_string());
let filter = TestFilter::new(&args);
let functions = vec![
create_test_function("test_api_endpoint", "api_test", TestType::Integration, vec![]),
create_test_function("test_database_query", "db_test", TestType::Integration, vec![]),
];
let filtered = filter.filter_functions(functions);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "test_api_endpoint");
}
}