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 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 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 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 pub fn adapter(&self, index: usize) -> &dyn TestAdapter {
102 self.adapters[index].as_ref()
103 }
104
105 pub fn adapters(&self) -> &[Box<dyn TestAdapter>] {
107 &self.adapters
108 }
109
110 pub const BUILTIN_COUNT: usize = 11;
113
114 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 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}