cargo-test-filter 0.1.0

A cargo subcommand for intelligent test filtering and compilation
Documentation
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 }
    }

    /// Filter individual test functions based on the provided arguments
    pub fn filter_functions(&self, functions: Vec<TestFunction>) -> Vec<TestFunction> {
        functions
            .into_iter()
            .filter(|func| self.matches_function(func))
            .collect()
    }

    /// Check if a test function matches all filter criteria
    fn matches_function(&self, func: &TestFunction) -> bool {
        // Filter by test type
        if self.args.integration && func.test_type != TestType::Integration {
            return false;
        }
        if self.args.unit && func.test_type != TestType::Unit {
            return false;
        }

        // Filter by tags (include)
        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;
            }
        }

        // Filter by tags (exclude)
        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;
            }
        }

        // Filter by name pattern (matches function name)
        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;
                }
            }
        }

        // Filter by path pattern
        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
    }

    /// Legacy: Filter tests based on the provided arguments (file-level)
    pub fn filter_tests(&self, tests: Vec<TestTarget>) -> Vec<TestTarget> {
        tests
            .into_iter()
            .filter(|test| self.matches_test(test))
            .collect()
    }

    /// Legacy: Check if a test matches all filter criteria (file-level)
    fn matches_test(&self, test: &TestTarget) -> bool {
        // Filter by test type
        if self.args.integration && test.test_type != TestType::Integration {
            return false;
        }
        if self.args.unit && test.test_type != TestType::Unit {
            return false;
        }

        // Filter by tags (include)
        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;
            }
        }

        // Filter by tags (exclude)
        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;
            }
        }

        // Filter by name pattern
        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;
                }
            }
        }

        // Filter by path pattern
        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
    }

    /// Get a summary of what filters are active
    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");
    }
}