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
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn detect_rust_project() {
117        let dir = tempfile::tempdir().unwrap();
118        std::fs::write(
119            dir.path().join("Cargo.toml"),
120            "[package]\nname = \"test\"\n",
121        )
122        .unwrap();
123        let engine = DetectionEngine::new();
124        let det = engine.detect(dir.path()).unwrap();
125        assert_eq!(det.detection.language, "Rust");
126    }
127
128    #[test]
129    fn detect_go_project() {
130        let dir = tempfile::tempdir().unwrap();
131        std::fs::write(dir.path().join("go.mod"), "module example.com/test\n").unwrap();
132        std::fs::write(dir.path().join("main_test.go"), "package main\n").unwrap();
133        let engine = DetectionEngine::new();
134        let det = engine.detect(dir.path()).unwrap();
135        assert_eq!(det.detection.language, "Go");
136    }
137
138    #[test]
139    fn detect_python_project() {
140        let dir = tempfile::tempdir().unwrap();
141        std::fs::write(dir.path().join("pyproject.toml"), "[tool.pytest]\n").unwrap();
142        let engine = DetectionEngine::new();
143        let det = engine.detect(dir.path()).unwrap();
144        assert_eq!(det.detection.language, "Python");
145    }
146
147    #[test]
148    fn detect_js_project() {
149        let dir = tempfile::tempdir().unwrap();
150        std::fs::write(
151            dir.path().join("package.json"),
152            r#"{"devDependencies":{"jest":"^29"}}"#,
153        )
154        .unwrap();
155        std::fs::write(dir.path().join("jest.config.js"), "").unwrap();
156        let engine = DetectionEngine::new();
157        let det = engine.detect(dir.path()).unwrap();
158        assert_eq!(det.detection.language, "JavaScript");
159    }
160
161    #[test]
162    fn detect_nothing_in_empty_dir() {
163        let dir = tempfile::tempdir().unwrap();
164        let engine = DetectionEngine::new();
165        assert!(engine.detect(dir.path()).is_none());
166    }
167
168    #[test]
169    fn detect_all_polyglot() {
170        let dir = tempfile::tempdir().unwrap();
171        // Both Rust and Python
172        std::fs::write(
173            dir.path().join("Cargo.toml"),
174            "[package]\nname = \"test\"\n",
175        )
176        .unwrap();
177        std::fs::write(dir.path().join("pyproject.toml"), "[tool.pytest]\n").unwrap();
178        let engine = DetectionEngine::new();
179        let all = engine.detect_all(dir.path());
180        assert!(all.len() >= 2);
181    }
182
183    #[test]
184    fn adapter_count() {
185        let engine = DetectionEngine::new();
186        assert_eq!(engine.adapters().len(), 11);
187    }
188}