cargo-test-filter 0.1.1

A cargo subcommand for intelligent test filtering and compilation
Documentation
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::process::{Command, Stdio};
use crate::cli::TestFilterArgs;
use crate::discovery::{TestFunction, TestTarget, TestType};

pub struct TestRunner<'a> {
    args: &'a TestFilterArgs,
}

impl<'a> TestRunner<'a> {
    pub fn new(args: &'a TestFilterArgs) -> Self {
        Self { args }
    }

    /// List the test functions without running them
    pub fn list_test_functions(&self, functions: &[TestFunction]) {
        if functions.is_empty() {
            println!("No tests match the filter criteria.");
            return;
        }

        println!("Found {} test function(s):\n", functions.len());

        // Group by test type for cleaner output
        let mut integration_tests: HashMap<String, Vec<&TestFunction>> = HashMap::new();
        let mut unit_tests: Vec<&TestFunction> = Vec::new();

        for func in functions {
            match func.test_type {
                TestType::Integration => {
                    integration_tests
                        .entry(func.target_name.clone())
                        .or_default()
                        .push(func);
                }
                TestType::Unit => {
                    unit_tests.push(func);
                }
                TestType::Doc => {}
            }
        }

        // Print integration tests
        if !integration_tests.is_empty() {
            println!("Integration Tests:");
            for (target, funcs) in &integration_tests {
                println!("  tests/{}.rs:", target);
                for func in funcs {
                    if func.tags.is_empty() {
                        println!("    - {}", func.name);
                    } else {
                        println!("    - {} [{}]", func.name, func.tags.join(", "));
                    }
                }
            }
            println!();
        }

        // Print unit tests
        if !unit_tests.is_empty() {
            println!("Unit Tests:");
            for func in &unit_tests {
                if func.tags.is_empty() {
                    println!("  - {}", func.name);
                } else {
                    println!("  - {} [{}]", func.name, func.tags.join(", "));
                }
            }
            println!();
        }
    }

    /// Run the filtered test functions using cargo test
    /// This is the primary method that provides function-level filtering
    pub fn run_test_functions(&self, functions: &[TestFunction]) -> Result<()> {
        if functions.is_empty() {
            println!("No tests match the filter criteria.");
            return Ok(());
        }

        println!("Running {} test function(s)...", functions.len());
        if self.args.verbose {
            for func in functions {
                println!("  - {}::{} (tags: {:?})", func.target_name, func.name, func.tags);
            }
            println!();
        }

        // Group functions by test type and target
        let mut integration_tests: HashMap<String, Vec<&TestFunction>> = HashMap::new();
        let mut unit_tests: Vec<&TestFunction> = Vec::new();

        for func in functions {
            match func.test_type {
                TestType::Integration => {
                    integration_tests
                        .entry(func.target_name.clone())
                        .or_default()
                        .push(func);
                }
                TestType::Unit => {
                    unit_tests.push(func);
                }
                TestType::Doc => {
                    // Doc tests are not yet supported
                }
            }
        }

        // Run integration tests grouped by target file
        for (target_name, funcs) in &integration_tests {
            self.run_integration_test_functions(target_name, funcs)?;
        }

        // Run unit tests
        if !unit_tests.is_empty() {
            self.run_unit_test_functions(&unit_tests)?;
        }

        Ok(())
    }

    /// Run specific integration test functions from a target
    fn run_integration_test_functions(&self, target_name: &str, functions: &[&TestFunction]) -> Result<()> {
        // For integration tests, use --exact since test names are just function names
        for func in functions {
            let mut cmd = Command::new("cargo");
            cmd.arg("test");
            cmd.arg("--test");
            cmd.arg(target_name);
            cmd.arg("--");
            cmd.arg("--exact");
            cmd.arg(&func.name);

            // Add verbose flag if needed
            if self.args.verbose {
                cmd.arg("--nocapture");
            }

            // Add any additional test arguments
            for arg in &self.args.test_args {
                cmd.arg(arg);
            }

            if self.args.verbose {
                println!("Executing: {:?}\n", cmd);
            }

            let status = cmd
                .stdin(Stdio::inherit())
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()
                .context("Failed to execute cargo test")?;

            if !status.success() {
                anyhow::bail!("Tests failed with exit code: {:?}", status.code());
            }
        }

        Ok(())
    }

    /// Run specific unit test functions
    fn run_unit_test_functions(&self, functions: &[&TestFunction]) -> Result<()> {
        // For unit tests, use substring matching since test names include module paths
        // e.g., "tests::test_basic_math" - we match on "test_basic_math"
        for func in functions {
            let mut cmd = Command::new("cargo");
            cmd.arg("test");
            cmd.arg("--lib");
            // Use the function name as a substring filter (ends with ::func_name)
            cmd.arg(format!("::{}", func.name));
            cmd.arg("--");

            // Add verbose flag if needed
            if self.args.verbose {
                cmd.arg("--nocapture");
            }

            // Add any additional test arguments
            for arg in &self.args.test_args {
                cmd.arg(arg);
            }

            if self.args.verbose {
                println!("Executing: {:?}\n", cmd);
            }

            let status = cmd
                .stdin(Stdio::inherit())
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()
                .context("Failed to execute cargo test")?;

            if !status.success() {
                anyhow::bail!("Tests failed with exit code: {:?}", status.code());
            }
        }

        Ok(())
    }

    /// Legacy: Run the filtered tests using cargo test (file-level)
    pub fn run_tests(&self, targets: &[TestTarget]) -> Result<()> {
        if targets.is_empty() {
            println!("No tests match the filter criteria.");
            return Ok(());
        }

        println!("Running {} test target(s)...", targets.len());
        if self.args.verbose {
            for target in targets {
                println!("  - {} ({:?})", target.name, target.test_type);
            }
        }

        // If we have integration tests, run each one separately or run them together
        if self.args.integration {
            // Run all integration tests together
            self.run_integration_tests(targets)
        } else if self.args.unit {
            // Run unit tests
            self.run_unit_tests(targets)
        } else {
            // Mixed or no specific type - run general test command
            self.run_general_tests(targets)
        }
    }

    /// Legacy: Run integration tests
    fn run_integration_tests(&self, targets: &[TestTarget]) -> Result<()> {
        for target in targets {
            let mut cmd = Command::new("cargo");
            cmd.arg("test");
            cmd.arg("--test");
            cmd.arg(&target.name);

            // Add name filter if specified
            if let Some(ref name) = self.args.name {
                cmd.arg(name);
            }

            self.add_test_args(&mut cmd);

            if self.args.verbose {
                println!("\nExecuting: {:?}\n", cmd);
            }

            let status = cmd
                .stdin(Stdio::inherit())
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()
                .context("Failed to execute cargo test")?;

            if !status.success() {
                anyhow::bail!("Tests failed with exit code: {:?}", status.code());
            }
        }
        Ok(())
    }

    /// Legacy: Run unit tests
    fn run_unit_tests(&self, _targets: &[TestTarget]) -> Result<()> {
        let mut cmd = Command::new("cargo");
        cmd.arg("test");
        cmd.arg("--lib");

        // Add name filter if specified
        if let Some(ref name) = self.args.name {
            cmd.arg(name);
        }

        self.add_test_args(&mut cmd);

        if self.args.verbose {
            println!("\nExecuting: {:?}\n", cmd);
        }

        let status = cmd
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .status()
            .context("Failed to execute cargo test")?;

        if !status.success() {
            anyhow::bail!("Tests failed with exit code: {:?}", status.code());
        }

        Ok(())
    }

    /// Legacy: Run general tests (no specific type filter)
    fn run_general_tests(&self, _targets: &[TestTarget]) -> Result<()> {
        let mut cmd = Command::new("cargo");
        cmd.arg("test");

        // Add name filter if specified
        if let Some(ref name) = self.args.name {
            cmd.arg(name);
        }

        self.add_test_args(&mut cmd);

        if self.args.verbose {
            println!("\nExecuting: {:?}\n", cmd);
        }

        let status = cmd
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .status()
            .context("Failed to execute cargo test")?;

        if !status.success() {
            anyhow::bail!("Tests failed with exit code: {:?}", status.code());
        }

        Ok(())
    }

    /// Add test arguments to command
    fn add_test_args(&self, cmd: &mut Command) {
        let mut needs_separator = true;

        // Add verbose flag if needed
        if self.args.verbose {
            if needs_separator {
                cmd.arg("--");
                needs_separator = false;
            }
            cmd.arg("--nocapture");
        }

        // Add any additional test arguments
        if !self.args.test_args.is_empty() {
            if needs_separator {
                cmd.arg("--");
            }
            for arg in &self.args.test_args {
                cmd.arg(arg);
            }
        }
    }

    /// Run all tests without filtering (fallback)
    pub fn run_all_tests(&self) -> Result<()> {
        println!("Running all tests...");

        let mut cmd = Command::new("cargo");
        cmd.arg("test");

        self.add_test_args(&mut cmd);

        let status = cmd
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .status()
            .context("Failed to execute cargo test")?;

        if !status.success() {
            anyhow::bail!("Tests failed with exit code: {:?}", status.code());
        }

        Ok(())
    }
}