testlint_sdk/test_orchestrator/
mod.rs1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::time::{Duration, SystemTime};
5
6use crate::common::Language;
7
8mod cpp;
10mod csharp;
11mod go;
12mod java;
13mod javascript;
14mod php;
15mod python;
16mod ruby;
17mod rust_lang;
18mod utils;
19
20#[derive(Debug, Clone)]
23pub struct TestConfig {
24 pub language: Language,
25 pub source_paths: Vec<String>,
26 pub output_dir: PathBuf,
27 pub output_format: CoverageFormat,
28 pub include_patterns: Vec<String>,
29 pub exclude_patterns: Vec<String>,
30 pub branch_coverage: bool,
31 pub tool_version: Option<String>, pub command_timeout_secs: Option<u64>, pub retry_attempts: u32, }
35
36#[derive(Debug, Clone)]
37pub enum CoverageFormat {
38 Json,
39 Xml,
40 Lcov,
41 Html,
42}
43
44impl Default for TestConfig {
45 fn default() -> Self {
46 Self {
47 language: Language::Python,
48 source_paths: vec![],
49 output_dir: PathBuf::from("coverage"),
50 output_format: CoverageFormat::Json,
51 include_patterns: vec![],
52 exclude_patterns: vec![
53 "*/tests/*".to_string(),
55 "*/test/*".to_string(),
56 "*/__tests__/*".to_string(),
57 "*/.git/*".to_string(),
59 "*/.idea/*".to_string(),
60 "*/.vscode/*".to_string(),
61 ],
62 branch_coverage: true,
63 tool_version: None, command_timeout_secs: Some(300), retry_attempts: 3, }
67 }
68}
69
70pub struct TestOrchestrator {
71 pub(crate) config: TestConfig,
72}
73
74impl TestOrchestrator {
75 pub fn new(config: TestConfig) -> Self {
76 Self { config }
77 }
78
79 pub fn run_with_coverage(
81 &self,
82 command: &str,
83 args: &[&str],
84 ) -> Result<CoverageResult, String> {
85 fs::create_dir_all(&self.config.output_dir)
87 .map_err(|e| format!("Failed to create output directory: {}", e))?;
88
89 let start_time = SystemTime::now();
90
91 let result = match self.config.language {
92 Language::Python => self.run_python_coverage(command, args)?,
93 Language::Java => self.run_java_coverage(command, args)?,
94 Language::JavaScript | Language::TypeScript => {
95 self.run_javascript_coverage(command, args)?
96 }
97 Language::Go => self.run_go_coverage(command, args)?,
98 Language::Rust => self.run_rust_coverage(command, args)?,
99 Language::CSharp => self.run_csharp_coverage(command, args)?,
100 Language::Ruby => self.run_ruby_coverage(command, args)?,
101 Language::Php => self.run_php_coverage(command, args)?,
102 Language::Cpp => self.run_cpp_coverage(command, args)?,
103 };
104
105 let duration = start_time
106 .elapsed()
107 .unwrap_or(Duration::from_secs(0))
108 .as_secs();
109
110 Ok(CoverageResult {
111 language: self.config.language.clone(),
112 coverage_file: result,
113 duration_secs: duration,
114 timestamp: chrono::Utc::now().to_rfc3339(),
115 })
116 }
117
118 pub fn show_summary(&self) -> Result<(), String> {
120 match self.config.language {
121 Language::Python => {
122 println!("\nš Coverage Summary:");
123 let output = Command::new("coverage")
124 .arg("report")
125 .output()
126 .map_err(|e| format!("Failed to show coverage report: {}", e))?;
127
128 if output.status.success() {
129 println!("{}", String::from_utf8_lossy(&output.stdout));
130 }
131 }
132 Language::JavaScript | Language::TypeScript => {
133 println!("\nš Coverage Summary:");
134 let output = Command::new("nyc")
135 .arg("report")
136 .arg("--reporter=text")
137 .output()
138 .map_err(|e| format!("Failed to show coverage report: {}", e))?;
139
140 if output.status.success() {
141 println!("{}", String::from_utf8_lossy(&output.stdout));
142 } else {
143 println!(
145 "Coverage data generated in: {}",
146 self.config.output_dir.display()
147 );
148 }
149 }
150 Language::Java => {
151 println!("\nš Coverage Summary:");
152
153 let cli_path = dirs::home_dir()
154 .map(|h| h.join(".jacoco/jacococli.jar"))
155 .ok_or_else(|| "Could not determine JaCoCo CLI path".to_string())?;
156
157 let exec_file = self.config.output_dir.join("jacoco.exec");
158
159 if !exec_file.exists() {
160 println!("JaCoCo execution data not found: {}", exec_file.display());
161 return Ok(());
162 }
163
164 let mut cmd = Command::new("java");
165 cmd.arg("-jar").arg(&cli_path).arg("report");
166 cmd.arg(&exec_file);
167
168 let class_paths = vec![
170 PathBuf::from("target/classes"),
171 PathBuf::from("build/classes"),
172 PathBuf::from("classes"),
173 PathBuf::from("target"),
174 PathBuf::from("build"),
175 PathBuf::from("out"),
176 PathBuf::from("bin"),
177 PathBuf::from("examples"),
178 ];
179
180 for class_path in class_paths {
181 if class_path.exists() && class_path.is_dir() {
182 cmd.arg("--classfiles").arg(&class_path);
183 }
184 }
185
186 let output = cmd
187 .output()
188 .map_err(|e| format!("Failed to show coverage report: {}", e))?;
189
190 if output.status.success() {
191 println!("{}", String::from_utf8_lossy(&output.stdout));
192 } else {
193 println!(
194 "Coverage data generated in: {}",
195 self.config.output_dir.display()
196 );
197 println!(
198 "View HTML report: open {}/jacoco-html/index.html",
199 self.config.output_dir.display()
200 );
201 }
202 }
203 Language::Go => {
204 println!("\nš Coverage Summary:");
205
206 let coverage_file = self.config.output_dir.join("coverage.out");
207
208 if !coverage_file.exists() {
209 println!("Go coverage data not found: {}", coverage_file.display());
210 return Ok(());
211 }
212
213 let output = Command::new("go")
215 .arg("tool")
216 .arg("cover")
217 .arg(format!("-func={}", coverage_file.display()))
218 .output()
219 .map_err(|e| format!("Failed to show coverage report: {}", e))?;
220
221 if output.status.success() {
222 println!("{}", String::from_utf8_lossy(&output.stdout));
223 } else {
224 println!(
225 "Coverage data generated in: {}",
226 self.config.output_dir.display()
227 );
228 let html_file = self.config.output_dir.join("coverage.html");
229 if html_file.exists() {
230 println!("View HTML report: open {}", html_file.display());
231 }
232 }
233 }
234 Language::Rust => {
235 println!("\nš Coverage Summary:");
236
237 println!(
240 "Coverage report generated in: {}",
241 self.config.output_dir.display()
242 );
243
244 let html_dir = self.config.output_dir.join("html");
246 if html_dir.exists() {
247 println!("View HTML report: open {}/index.html", html_dir.display());
248 }
249
250 let lcov_file = self.config.output_dir.join("lcov.info");
252 if lcov_file.exists() {
253 println!("LCOV file: {}", lcov_file.display());
254 }
255 }
256 Language::CSharp => {
257 println!("\nš Coverage Summary:");
258
259 let cobertura_file = self.config.output_dir.join("cobertura.xml");
261 if cobertura_file.exists() {
262 println!("Coverage report generated: {}", cobertura_file.display());
263 }
264
265 let html_dir = self.config.output_dir.join("html");
267 if html_dir.exists() {
268 let index_file = html_dir.join("index.html");
269 if index_file.exists() {
270 println!("View HTML report: open {}", index_file.display());
271 }
272 }
273
274 let lcov_file = self.config.output_dir.join("lcov.info");
276 if lcov_file.exists() {
277 println!("LCOV file: {}", lcov_file.display());
278 }
279
280 let json_file = self.config.output_dir.join("coverage.json");
282 if json_file.exists() {
283 println!("JSON file: {}", json_file.display());
284 }
285 }
286 Language::Ruby => {
287 println!("\nš Coverage Summary:");
288
289 println!(
291 "Coverage report generated in: {}",
292 self.config.output_dir.display()
293 );
294
295 let html_dir = self.config.output_dir.join("html");
297 if html_dir.exists() {
298 let index_file = html_dir.join("index.html");
299 if index_file.exists() {
300 println!("View HTML report: open {}", index_file.display());
301 }
302 }
303
304 let lcov_file = self.config.output_dir.join("lcov.info");
306 if lcov_file.exists() {
307 println!("LCOV file: {}", lcov_file.display());
308 }
309
310 let json_file = self.config.output_dir.join("coverage.json");
312 if json_file.exists() {
313 println!("JSON file: {}", json_file.display());
314 }
315
316 let xml_file = self.config.output_dir.join("cobertura.xml");
318 if xml_file.exists() {
319 println!("XML file: {}", xml_file.display());
320 }
321 }
322 Language::Php => {
323 println!("\nš Coverage Summary:");
324
325 println!(
327 "Coverage report generated in: {}",
328 self.config.output_dir.display()
329 );
330
331 let html_dir = self.config.output_dir.join("html");
333 if html_dir.exists() {
334 let index_file = html_dir.join("index.html");
335 if index_file.exists() {
336 println!("View HTML report: open {}", index_file.display());
337 }
338 }
339
340 let xml_file = self.config.output_dir.join("coverage.xml");
342 if xml_file.exists() {
343 println!("Clover XML file: {}", xml_file.display());
344 }
345
346 let lcov_file = self.config.output_dir.join("lcov.info");
348 if lcov_file.exists() {
349 println!("LCOV file: {}", lcov_file.display());
350 }
351
352 let json_file = self.config.output_dir.join("coverage.json");
354 if json_file.exists() {
355 println!("JSON file: {}", json_file.display());
356 }
357 }
358 Language::Cpp => {
359 println!("\nš Coverage Summary:");
360
361 println!(
363 "Coverage report generated in: {}",
364 self.config.output_dir.display()
365 );
366
367 let html_dir = self.config.output_dir.join("html");
369 if html_dir.exists() {
370 let index_file = html_dir.join("index.html");
371 if index_file.exists() {
372 println!("View HTML report: open {}", index_file.display());
373 }
374 }
375
376 let lcov_file = self.config.output_dir.join("lcov.info");
378 if lcov_file.exists() {
379 println!("LCOV file: {}", lcov_file.display());
380 }
381
382 let xml_file = self.config.output_dir.join("coverage.xml");
384 if xml_file.exists() {
385 println!("XML file: {}", xml_file.display());
386 }
387
388 let json_file = self.config.output_dir.join("coverage.json");
390 if json_file.exists() {
391 println!("JSON file: {}", json_file.display());
392 }
393 }
394 }
395
396 Ok(())
397 }
398}
399
400#[derive(Debug)]
401pub struct CoverageResult {
402 pub language: Language,
403 pub coverage_file: PathBuf,
404 pub duration_secs: u64,
405 pub timestamp: String,
406}
407
408use std::process::Output;
410
411pub(crate) fn format_command_error(cmd_name: &str, output: &Output) -> String {
413 let stderr = String::from_utf8_lossy(&output.stderr);
414 let exit_code = output.status.code().unwrap_or(-1);
415
416 if stderr.trim().is_empty() {
417 format!("{} command failed with exit code {}", cmd_name, exit_code)
418 } else {
419 format!(
420 "{} command failed with exit code {}:\n{}",
421 cmd_name,
422 exit_code,
423 stderr.trim()
424 )
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_default_config() {
434 let config = TestConfig::default();
435 assert!(matches!(config.language, Language::Python));
436 assert_eq!(config.output_dir, PathBuf::from("coverage"));
437 assert!(config.branch_coverage);
438 }
439
440 #[test]
441 fn test_orchestrator_creation() {
442 let config = TestConfig::default();
443 let _orchestrator = TestOrchestrator::new(config);
444 }
445}