Skip to main content

code_analyze_core/
test_detection.rs

1// SPDX-FileCopyrightText: 2026 code-analyze-mcp contributors
2// SPDX-License-Identifier: Apache-2.0
3//! Test file detection using path heuristics.
4//!
5//! Identifies test files based on directory and filename patterns.
6//! Supports Rust, Python, Go, Java, TypeScript, and JavaScript.
7//! Note: Fortran has no test-file naming conventions and is not covered by these heuristics.
8
9use std::path::Path;
10
11/// Detect if a file path represents a test file based on path-based heuristics.
12///
13/// Checks for:
14/// - Directory patterns: tests/, test/, `__tests__`/, spec/
15/// - Filename patterns:
16///   - Rust: `test_*.rs`, `*_test.rs`
17///   - Python: `test_*.py`, `*_test.py`
18///   - Go: `*_test.go`
19///   - Java: `Test*.java`, `*Test.java`
20///   - TypeScript/JavaScript: `*.test.ts`, `*.test.js`, `*.spec.ts`, `*.spec.js`
21///
22/// Returns true if the path matches any test heuristic, false otherwise.
23#[must_use]
24pub fn is_test_file(path: &Path) -> bool {
25    // Check directory components for test directories
26    for component in path.components() {
27        if let Some("tests" | "test" | "__tests__" | "spec") = component.as_os_str().to_str() {
28            return true;
29        }
30    }
31
32    // Check filename patterns
33    let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
34        return false;
35    };
36
37    // Rust patterns
38    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
39    if file_name.starts_with("test_") && ext.eq_ignore_ascii_case("rs") {
40        return true;
41    }
42    if file_name.ends_with("_test.rs") {
43        return true;
44    }
45
46    // Python patterns
47    if file_name.starts_with("test_") && ext.eq_ignore_ascii_case("py") {
48        return true;
49    }
50    if file_name.ends_with("_test.py") {
51        return true;
52    }
53
54    // Go patterns
55    if file_name.ends_with("_test.go") {
56        return true;
57    }
58
59    // Java patterns
60    if file_name.starts_with("Test") && ext.eq_ignore_ascii_case("java") {
61        return true;
62    }
63    if file_name.ends_with("Test.java") {
64        return true;
65    }
66
67    // TypeScript/JavaScript patterns
68    if file_name.ends_with(".test.ts") || file_name.ends_with(".test.js") {
69        return true;
70    }
71    if file_name.ends_with(".spec.ts") || file_name.ends_with(".spec.js") {
72        return true;
73    }
74
75    false
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn filename_pattern_detects_test_file() {
84        assert!(is_test_file(Path::new("test_utils.rs")));
85        assert!(is_test_file(Path::new("utils_test.rs")));
86    }
87
88    #[test]
89    fn filename_pattern_rejects_production_file() {
90        assert!(!is_test_file(Path::new("utils.rs")));
91        assert!(!is_test_file(Path::new("main.rs")));
92    }
93
94    #[test]
95    fn directory_pattern_detects_test_file() {
96        assert!(is_test_file(Path::new("tests/utils.rs")));
97    }
98
99    #[test]
100    fn directory_pattern_detects_nested_test_file() {
101        assert!(is_test_file(Path::new("src/tests/utils.rs")));
102        assert!(!is_test_file(Path::new("src/utils.rs")));
103    }
104}