Skip to main content

testx/detection/
mod.rs

1use std::path::Path;
2
3use crate::adapters::cpp::CppAdapter;
4use crate::adapters::dotnet::DotnetAdapter;
5use crate::adapters::elixir::ElixirAdapter;
6use crate::adapters::go::GoAdapter;
7use crate::adapters::java::JavaAdapter;
8use crate::adapters::javascript::JavaScriptAdapter;
9use crate::adapters::php::PhpAdapter;
10use crate::adapters::python::PythonAdapter;
11use crate::adapters::ruby::RubyAdapter;
12use crate::adapters::rust::RustAdapter;
13use crate::adapters::zig::ZigAdapter;
14use crate::adapters::{DetectionResult, TestAdapter};
15
16pub struct DetectionEngine {
17    adapters: Vec<Box<dyn TestAdapter>>,
18}
19
20pub struct DetectedProject {
21    pub detection: DetectionResult,
22    pub adapter_index: usize,
23}
24
25impl Default for DetectionEngine {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl DetectionEngine {
32    pub fn new() -> Self {
33        Self {
34            adapters: vec![
35                Box::new(RustAdapter::new()),
36                Box::new(GoAdapter::new()),
37                Box::new(PythonAdapter::new()),
38                Box::new(JavaScriptAdapter::new()),
39                Box::new(JavaAdapter::new()),
40                Box::new(CppAdapter::new()),
41                Box::new(RubyAdapter::new()),
42                Box::new(ElixirAdapter::new()),
43                Box::new(PhpAdapter::new()),
44                Box::new(DotnetAdapter::new()),
45                Box::new(ZigAdapter::new()),
46            ],
47        }
48    }
49
50    /// Detect the best matching test framework for the given project directory.
51    /// Returns the detection result and a reference to the matching adapter.
52    pub fn detect(&self, project_dir: &Path) -> Option<DetectedProject> {
53        let mut best: Option<DetectedProject> = None;
54
55        for (i, adapter) in self.adapters.iter().enumerate() {
56            if let Some(result) = adapter.detect(project_dir) {
57                let dominated = best
58                    .as_ref()
59                    .map(|b| result.confidence > b.detection.confidence)
60                    .unwrap_or(true);
61                if dominated {
62                    best = Some(DetectedProject {
63                        detection: result,
64                        adapter_index: i,
65                    });
66                    // Early exit on very high confidence — no need to scan remaining adapters
67                    if best
68                        .as_ref()
69                        .is_some_and(|b| b.detection.confidence >= 0.95)
70                    {
71                        break;
72                    }
73                }
74            }
75        }
76
77        best
78    }
79
80    /// Detect all matching frameworks (for polyglot projects).
81    pub fn detect_all(&self, project_dir: &Path) -> Vec<DetectedProject> {
82        let mut results = Vec::new();
83        for (i, adapter) in self.adapters.iter().enumerate() {
84            if let Some(result) = adapter.detect(project_dir) {
85                results.push(DetectedProject {
86                    detection: result,
87                    adapter_index: i,
88                });
89            }
90        }
91        results.sort_by(|a, b| {
92            b.detection
93                .confidence
94                .partial_cmp(&a.detection.confidence)
95                .unwrap_or(std::cmp::Ordering::Equal)
96        });
97        results
98    }
99
100    /// Get an adapter by index.
101    pub fn adapter(&self, index: usize) -> &dyn TestAdapter {
102        self.adapters[index].as_ref()
103    }
104
105    /// Get all registered adapters.
106    pub fn adapters(&self) -> &[Box<dyn TestAdapter>] {
107        &self.adapters
108    }
109
110    /// Number of built-in adapters (registered at construction time).
111    /// Custom adapters are appended after these.
112    pub const BUILTIN_COUNT: usize = 11;
113
114    /// Register a custom adapter. Custom adapters are appended after built-in
115    /// ones and participate in normal confidence-based detection.
116    pub fn register(&mut self, adapter: Box<dyn TestAdapter>) {
117        self.adapters.push(adapter);
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn detect_rust_project() {
127        let dir = tempfile::tempdir().unwrap();
128        std::fs::write(
129            dir.path().join("Cargo.toml"),
130            "[package]\nname = \"test\"\n",
131        )
132        .unwrap();
133        let engine = DetectionEngine::new();
134        let det = engine.detect(dir.path()).unwrap();
135        assert_eq!(det.detection.language, "Rust");
136    }
137
138    #[test]
139    fn detect_go_project() {
140        let dir = tempfile::tempdir().unwrap();
141        std::fs::write(dir.path().join("go.mod"), "module example.com/test\n").unwrap();
142        std::fs::write(dir.path().join("main_test.go"), "package main\n").unwrap();
143        let engine = DetectionEngine::new();
144        let det = engine.detect(dir.path()).unwrap();
145        assert_eq!(det.detection.language, "Go");
146    }
147
148    #[test]
149    fn detect_python_project() {
150        let dir = tempfile::tempdir().unwrap();
151        std::fs::write(dir.path().join("pyproject.toml"), "[tool.pytest]\n").unwrap();
152        let engine = DetectionEngine::new();
153        let det = engine.detect(dir.path()).unwrap();
154        assert_eq!(det.detection.language, "Python");
155    }
156
157    #[test]
158    fn detect_js_project() {
159        let dir = tempfile::tempdir().unwrap();
160        std::fs::write(
161            dir.path().join("package.json"),
162            r#"{"devDependencies":{"jest":"^29"}}"#,
163        )
164        .unwrap();
165        std::fs::write(dir.path().join("jest.config.js"), "").unwrap();
166        let engine = DetectionEngine::new();
167        let det = engine.detect(dir.path()).unwrap();
168        assert_eq!(det.detection.language, "JavaScript");
169    }
170
171    #[test]
172    fn detect_nothing_in_empty_dir() {
173        let dir = tempfile::tempdir().unwrap();
174        let engine = DetectionEngine::new();
175        assert!(engine.detect(dir.path()).is_none());
176    }
177
178    #[test]
179    fn detect_all_polyglot() {
180        let dir = tempfile::tempdir().unwrap();
181        // Both Rust and Python
182        std::fs::write(
183            dir.path().join("Cargo.toml"),
184            "[package]\nname = \"test\"\n",
185        )
186        .unwrap();
187        std::fs::write(dir.path().join("pyproject.toml"), "[tool.pytest]\n").unwrap();
188        let engine = DetectionEngine::new();
189        let all = engine.detect_all(dir.path());
190        assert!(all.len() >= 2);
191    }
192
193    #[test]
194    fn adapter_count() {
195        let engine = DetectionEngine::new();
196        assert_eq!(engine.adapters().len(), 11);
197    }
198}