Skip to main content

cargo_test_filter/
runner.rs

1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::process::{Command, Stdio};
4use crate::cli::TestFilterArgs;
5use crate::discovery::{TestFunction, TestTarget, TestType};
6
7pub struct TestRunner<'a> {
8    args: &'a TestFilterArgs,
9}
10
11impl<'a> TestRunner<'a> {
12    pub fn new(args: &'a TestFilterArgs) -> Self {
13        Self { args }
14    }
15
16    /// List the test functions without running them
17    pub fn list_test_functions(&self, functions: &[TestFunction]) {
18        if functions.is_empty() {
19            println!("No tests match the filter criteria.");
20            return;
21        }
22
23        println!("Found {} test function(s):\n", functions.len());
24
25        // Group by test type for cleaner output
26        let mut integration_tests: HashMap<String, Vec<&TestFunction>> = HashMap::new();
27        let mut unit_tests: Vec<&TestFunction> = Vec::new();
28
29        for func in functions {
30            match func.test_type {
31                TestType::Integration => {
32                    integration_tests
33                        .entry(func.target_name.clone())
34                        .or_default()
35                        .push(func);
36                }
37                TestType::Unit => {
38                    unit_tests.push(func);
39                }
40                TestType::Doc => {}
41            }
42        }
43
44        // Print integration tests
45        if !integration_tests.is_empty() {
46            println!("Integration Tests:");
47            for (target, funcs) in &integration_tests {
48                println!("  tests/{}.rs:", target);
49                for func in funcs {
50                    if func.tags.is_empty() {
51                        println!("    - {}", func.name);
52                    } else {
53                        println!("    - {} [{}]", func.name, func.tags.join(", "));
54                    }
55                }
56            }
57            println!();
58        }
59
60        // Print unit tests
61        if !unit_tests.is_empty() {
62            println!("Unit Tests:");
63            for func in &unit_tests {
64                if func.tags.is_empty() {
65                    println!("  - {}", func.name);
66                } else {
67                    println!("  - {} [{}]", func.name, func.tags.join(", "));
68                }
69            }
70            println!();
71        }
72    }
73
74    /// Run the filtered test functions using cargo test
75    /// This is the primary method that provides function-level filtering
76    pub fn run_test_functions(&self, functions: &[TestFunction]) -> Result<()> {
77        if functions.is_empty() {
78            println!("No tests match the filter criteria.");
79            return Ok(());
80        }
81
82        println!("Running {} test function(s)...", functions.len());
83        if self.args.verbose {
84            for func in functions {
85                println!("  - {}::{} (tags: {:?})", func.target_name, func.name, func.tags);
86            }
87            println!();
88        }
89
90        // Group functions by test type and target
91        let mut integration_tests: HashMap<String, Vec<&TestFunction>> = HashMap::new();
92        let mut unit_tests: Vec<&TestFunction> = Vec::new();
93
94        for func in functions {
95            match func.test_type {
96                TestType::Integration => {
97                    integration_tests
98                        .entry(func.target_name.clone())
99                        .or_default()
100                        .push(func);
101                }
102                TestType::Unit => {
103                    unit_tests.push(func);
104                }
105                TestType::Doc => {
106                    // Doc tests are not yet supported
107                }
108            }
109        }
110
111        // Run integration tests grouped by target file
112        for (target_name, funcs) in &integration_tests {
113            self.run_integration_test_functions(target_name, funcs)?;
114        }
115
116        // Run unit tests
117        if !unit_tests.is_empty() {
118            self.run_unit_test_functions(&unit_tests)?;
119        }
120
121        Ok(())
122    }
123
124    /// Run specific integration test functions from a target
125    fn run_integration_test_functions(&self, target_name: &str, functions: &[&TestFunction]) -> Result<()> {
126        // For integration tests, use --exact since test names are just function names
127        for func in functions {
128            let mut cmd = Command::new("cargo");
129            cmd.arg("test");
130            cmd.arg("--test");
131            cmd.arg(target_name);
132            cmd.arg("--");
133            cmd.arg("--exact");
134            cmd.arg(&func.name);
135
136            // Add verbose flag if needed
137            if self.args.verbose {
138                cmd.arg("--nocapture");
139            }
140
141            // Add any additional test arguments
142            for arg in &self.args.test_args {
143                cmd.arg(arg);
144            }
145
146            if self.args.verbose {
147                println!("Executing: {:?}\n", cmd);
148            }
149
150            let status = cmd
151                .stdin(Stdio::inherit())
152                .stdout(Stdio::inherit())
153                .stderr(Stdio::inherit())
154                .status()
155                .context("Failed to execute cargo test")?;
156
157            if !status.success() {
158                anyhow::bail!("Tests failed with exit code: {:?}", status.code());
159            }
160        }
161
162        Ok(())
163    }
164
165    /// Run specific unit test functions
166    fn run_unit_test_functions(&self, functions: &[&TestFunction]) -> Result<()> {
167        // For unit tests, use substring matching since test names include module paths
168        // e.g., "tests::test_basic_math" - we match on "test_basic_math"
169        for func in functions {
170            let mut cmd = Command::new("cargo");
171            cmd.arg("test");
172            cmd.arg("--lib");
173            // Use the function name as a substring filter (ends with ::func_name)
174            cmd.arg(format!("::{}", func.name));
175            cmd.arg("--");
176
177            // Add verbose flag if needed
178            if self.args.verbose {
179                cmd.arg("--nocapture");
180            }
181
182            // Add any additional test arguments
183            for arg in &self.args.test_args {
184                cmd.arg(arg);
185            }
186
187            if self.args.verbose {
188                println!("Executing: {:?}\n", cmd);
189            }
190
191            let status = cmd
192                .stdin(Stdio::inherit())
193                .stdout(Stdio::inherit())
194                .stderr(Stdio::inherit())
195                .status()
196                .context("Failed to execute cargo test")?;
197
198            if !status.success() {
199                anyhow::bail!("Tests failed with exit code: {:?}", status.code());
200            }
201        }
202
203        Ok(())
204    }
205
206    /// Legacy: Run the filtered tests using cargo test (file-level)
207    pub fn run_tests(&self, targets: &[TestTarget]) -> Result<()> {
208        if targets.is_empty() {
209            println!("No tests match the filter criteria.");
210            return Ok(());
211        }
212
213        println!("Running {} test target(s)...", targets.len());
214        if self.args.verbose {
215            for target in targets {
216                println!("  - {} ({:?})", target.name, target.test_type);
217            }
218        }
219
220        // If we have integration tests, run each one separately or run them together
221        if self.args.integration {
222            // Run all integration tests together
223            self.run_integration_tests(targets)
224        } else if self.args.unit {
225            // Run unit tests
226            self.run_unit_tests(targets)
227        } else {
228            // Mixed or no specific type - run general test command
229            self.run_general_tests(targets)
230        }
231    }
232
233    /// Legacy: Run integration tests
234    fn run_integration_tests(&self, targets: &[TestTarget]) -> Result<()> {
235        for target in targets {
236            let mut cmd = Command::new("cargo");
237            cmd.arg("test");
238            cmd.arg("--test");
239            cmd.arg(&target.name);
240
241            // Add name filter if specified
242            if let Some(ref name) = self.args.name {
243                cmd.arg(name);
244            }
245
246            self.add_test_args(&mut cmd);
247
248            if self.args.verbose {
249                println!("\nExecuting: {:?}\n", cmd);
250            }
251
252            let status = cmd
253                .stdin(Stdio::inherit())
254                .stdout(Stdio::inherit())
255                .stderr(Stdio::inherit())
256                .status()
257                .context("Failed to execute cargo test")?;
258
259            if !status.success() {
260                anyhow::bail!("Tests failed with exit code: {:?}", status.code());
261            }
262        }
263        Ok(())
264    }
265
266    /// Legacy: Run unit tests
267    fn run_unit_tests(&self, _targets: &[TestTarget]) -> Result<()> {
268        let mut cmd = Command::new("cargo");
269        cmd.arg("test");
270        cmd.arg("--lib");
271
272        // Add name filter if specified
273        if let Some(ref name) = self.args.name {
274            cmd.arg(name);
275        }
276
277        self.add_test_args(&mut cmd);
278
279        if self.args.verbose {
280            println!("\nExecuting: {:?}\n", cmd);
281        }
282
283        let status = cmd
284            .stdin(Stdio::inherit())
285            .stdout(Stdio::inherit())
286            .stderr(Stdio::inherit())
287            .status()
288            .context("Failed to execute cargo test")?;
289
290        if !status.success() {
291            anyhow::bail!("Tests failed with exit code: {:?}", status.code());
292        }
293
294        Ok(())
295    }
296
297    /// Legacy: Run general tests (no specific type filter)
298    fn run_general_tests(&self, _targets: &[TestTarget]) -> Result<()> {
299        let mut cmd = Command::new("cargo");
300        cmd.arg("test");
301
302        // Add name filter if specified
303        if let Some(ref name) = self.args.name {
304            cmd.arg(name);
305        }
306
307        self.add_test_args(&mut cmd);
308
309        if self.args.verbose {
310            println!("\nExecuting: {:?}\n", cmd);
311        }
312
313        let status = cmd
314            .stdin(Stdio::inherit())
315            .stdout(Stdio::inherit())
316            .stderr(Stdio::inherit())
317            .status()
318            .context("Failed to execute cargo test")?;
319
320        if !status.success() {
321            anyhow::bail!("Tests failed with exit code: {:?}", status.code());
322        }
323
324        Ok(())
325    }
326
327    /// Add test arguments to command
328    fn add_test_args(&self, cmd: &mut Command) {
329        let mut needs_separator = true;
330
331        // Add verbose flag if needed
332        if self.args.verbose {
333            if needs_separator {
334                cmd.arg("--");
335                needs_separator = false;
336            }
337            cmd.arg("--nocapture");
338        }
339
340        // Add any additional test arguments
341        if !self.args.test_args.is_empty() {
342            if needs_separator {
343                cmd.arg("--");
344            }
345            for arg in &self.args.test_args {
346                cmd.arg(arg);
347            }
348        }
349    }
350
351    /// Run all tests without filtering (fallback)
352    pub fn run_all_tests(&self) -> Result<()> {
353        println!("Running all tests...");
354
355        let mut cmd = Command::new("cargo");
356        cmd.arg("test");
357
358        self.add_test_args(&mut cmd);
359
360        let status = cmd
361            .stdin(Stdio::inherit())
362            .stdout(Stdio::inherit())
363            .stderr(Stdio::inherit())
364            .status()
365            .context("Failed to execute cargo test")?;
366
367        if !status.success() {
368            anyhow::bail!("Tests failed with exit code: {:?}", status.code());
369        }
370
371        Ok(())
372    }
373}