/home/noah/src/ruchy/src/quality/enforcement.rs
Line | Count | Source |
1 | | //! Quality gate enforcement implementation for CLI |
2 | | |
3 | | use std::path::Path; |
4 | | use anyhow::Result; |
5 | | use crate::quality::gates::{QualityGateEnforcer, QualityGateConfig}; |
6 | | use crate::quality::scoring::{ScoreEngine, AnalysisDepth}; |
7 | | |
8 | | /// Load and configure quality gate enforcer (complexity: 6) |
9 | 0 | fn load_gate_config( |
10 | 0 | path: &Path, |
11 | 0 | config: Option<&Path>, |
12 | 0 | ci: bool |
13 | 0 | ) -> Result<QualityGateEnforcer> { |
14 | | // Load configuration |
15 | 0 | let project_root = find_project_root(path)?; |
16 | 0 | let mut gate_config = if let Some(config_path) = config { |
17 | 0 | QualityGateEnforcer::load_config(config_path.parent().unwrap_or(Path::new("."))) |
18 | | } else { |
19 | 0 | QualityGateEnforcer::load_config(&project_root) |
20 | 0 | }?; |
21 | | |
22 | | // Apply CI mode overrides (stricter thresholds) |
23 | 0 | if ci { |
24 | 0 | gate_config = apply_ci_overrides(gate_config); |
25 | 0 | } |
26 | | |
27 | 0 | Ok(QualityGateEnforcer::new(gate_config)) |
28 | 0 | } |
29 | | |
30 | | /// Parse analysis depth string parameter (complexity: 4) |
31 | 0 | fn parse_analysis_depth(depth: &str) -> Result<AnalysisDepth> { |
32 | 0 | match depth { |
33 | 0 | "shallow" => Ok(AnalysisDepth::Shallow), |
34 | 0 | "standard" => Ok(AnalysisDepth::Standard), |
35 | 0 | "deep" => Ok(AnalysisDepth::Deep), |
36 | 0 | _ => Err(anyhow::anyhow!("Invalid depth: {}", depth)), |
37 | | } |
38 | 0 | } |
39 | | |
40 | | /// Process file or directory path (complexity: 5) |
41 | 0 | fn process_path( |
42 | 0 | path: &Path, |
43 | 0 | enforcer: &QualityGateEnforcer, |
44 | 0 | analysis_depth: AnalysisDepth, |
45 | 0 | fail_fast: bool, |
46 | 0 | verbose: bool |
47 | 0 | ) -> Result<Vec<crate::quality::gates::GateResult>> { |
48 | 0 | let mut all_results = Vec::new(); |
49 | | |
50 | 0 | if path.is_file() { |
51 | 0 | let result = process_file(enforcer, path, analysis_depth, verbose)?; |
52 | 0 | all_results.push(result); |
53 | 0 | } else if path.is_dir() { |
54 | 0 | let results = process_directory(enforcer, path, analysis_depth, fail_fast, verbose)?; |
55 | 0 | all_results.extend(results); |
56 | | } else { |
57 | 0 | return Err(anyhow::anyhow!("Invalid path: {}", path.display())); |
58 | | } |
59 | | |
60 | 0 | Ok(all_results) |
61 | 0 | } |
62 | | |
63 | | /// Output results in specified format (complexity: 4) |
64 | 0 | fn output_results( |
65 | 0 | results: &[crate::quality::gates::GateResult], |
66 | 0 | format: &str, |
67 | 0 | verbose: bool |
68 | 0 | ) -> Result<()> { |
69 | 0 | match format { |
70 | 0 | "console" => print_console_results(results, verbose)?, |
71 | 0 | "json" => print_json_results(results)?, |
72 | 0 | "junit" => print_junit_results(results)?, |
73 | 0 | _ => return Err(anyhow::anyhow!("Invalid format: {}", format)), |
74 | | } |
75 | 0 | Ok(()) |
76 | 0 | } |
77 | | |
78 | | /// Handle CI export if requested (complexity: 3) |
79 | 0 | fn handle_export( |
80 | 0 | enforcer: &QualityGateEnforcer, |
81 | 0 | results: &[crate::quality::gates::GateResult], |
82 | 0 | export: Option<&Path> |
83 | 0 | ) -> Result<()> { |
84 | 0 | if let Some(export_path) = export { |
85 | 0 | std::fs::create_dir_all(export_path)?; |
86 | 0 | enforcer.export_ci_results(results, export_path)?; |
87 | 0 | println!("📊 Results exported to {}", export_path.display()); |
88 | 0 | } |
89 | 0 | Ok(()) |
90 | 0 | } |
91 | | |
92 | | /// Check gate results and exit appropriately (complexity: 4) |
93 | 0 | fn check_gate_results(results: &[crate::quality::gates::GateResult]) -> Result<()> { |
94 | 0 | let failed_gates = results.iter().filter(|r| !r.passed).count(); |
95 | | |
96 | 0 | if failed_gates > 0 { |
97 | 0 | eprintln!("❌ {failed_gates} quality gate(s) failed"); |
98 | 0 | std::process::exit(1); |
99 | 0 | } else { |
100 | 0 | println!("✅ All quality gates passed!"); |
101 | 0 | } |
102 | | |
103 | 0 | Ok(()) |
104 | 0 | } |
105 | | |
106 | | /// Enforce quality gates on a file or directory (complexity: 6) |
107 | 0 | pub fn enforce_quality_gates( |
108 | 0 | path: &Path, |
109 | 0 | config: Option<&Path>, |
110 | 0 | depth: &str, |
111 | 0 | fail_fast: bool, |
112 | 0 | format: &str, |
113 | 0 | export: Option<&Path>, |
114 | 0 | ci: bool, |
115 | 0 | verbose: bool, |
116 | 0 | ) -> Result<()> { |
117 | 0 | let enforcer = load_gate_config(path, config, ci)?; |
118 | 0 | let analysis_depth = parse_analysis_depth(depth)?; |
119 | | |
120 | 0 | let all_results = process_path(path, &enforcer, analysis_depth, fail_fast, verbose)?; |
121 | | |
122 | 0 | output_results(&all_results, format, verbose)?; |
123 | 0 | handle_export(&enforcer, &all_results, export)?; |
124 | 0 | check_gate_results(&all_results)?; |
125 | | |
126 | 0 | Ok(()) |
127 | 0 | } |
128 | | |
129 | 0 | fn find_project_root(path: &Path) -> Result<std::path::PathBuf> { |
130 | 0 | let mut current = if path.is_file() { |
131 | 0 | path.parent().unwrap_or(Path::new(".")) |
132 | | } else { |
133 | 0 | path |
134 | | }; |
135 | | |
136 | | loop { |
137 | 0 | if current.join("Cargo.toml").exists() || current.join(".ruchy").exists() { |
138 | 0 | return Ok(current.to_path_buf()); |
139 | 0 | } |
140 | | |
141 | 0 | if let Some(parent) = current.parent() { |
142 | 0 | current = parent; |
143 | 0 | } else { |
144 | | // Default to current directory |
145 | 0 | return Ok(Path::new(".").to_path_buf()); |
146 | | } |
147 | | } |
148 | 0 | } |
149 | | |
150 | 0 | fn apply_ci_overrides(mut config: QualityGateConfig) -> QualityGateConfig { |
151 | | // Apply stricter thresholds for CI |
152 | 0 | config.min_score = config.min_score.max(0.8); // Higher overall score |
153 | 0 | config.component_thresholds.correctness = config.component_thresholds.correctness.max(0.9); |
154 | 0 | config.component_thresholds.safety = config.component_thresholds.safety.max(0.9); |
155 | 0 | config.anti_gaming.min_confidence = config.anti_gaming.min_confidence.max(0.8); |
156 | 0 | config.ci_integration.fail_on_violation = true; |
157 | 0 | config |
158 | 0 | } |
159 | | |
160 | 0 | fn process_file( |
161 | 0 | enforcer: &QualityGateEnforcer, |
162 | 0 | file_path: &Path, |
163 | 0 | depth: AnalysisDepth, |
164 | 0 | verbose: bool, |
165 | 0 | ) -> Result<crate::quality::gates::GateResult> { |
166 | 0 | if verbose { |
167 | 0 | println!("🔍 Analyzing {}", file_path.display()); |
168 | 0 | } |
169 | | |
170 | | // Read and parse file |
171 | 0 | let content = std::fs::read_to_string(file_path)?; |
172 | 0 | let mut parser = crate::Parser::new(&content); |
173 | 0 | let ast = parser.parse()?; |
174 | | |
175 | | // Calculate score |
176 | 0 | let score_config = crate::quality::scoring::ScoreConfig::default(); |
177 | 0 | let mut score_engine = ScoreEngine::new(score_config); |
178 | 0 | let score = score_engine.score_incremental(&ast, file_path.to_path_buf(), &content, depth); |
179 | | |
180 | | // Enforce gates |
181 | 0 | let result = enforcer.enforce_gates(&score, Some(&file_path.to_path_buf())); |
182 | | |
183 | 0 | Ok(result) |
184 | 0 | } |
185 | | |
186 | 0 | fn process_directory( |
187 | 0 | enforcer: &QualityGateEnforcer, |
188 | 0 | dir_path: &Path, |
189 | 0 | depth: AnalysisDepth, |
190 | 0 | fail_fast: bool, |
191 | 0 | verbose: bool, |
192 | 0 | ) -> Result<Vec<crate::quality::gates::GateResult>> { |
193 | 0 | let mut results = Vec::new(); |
194 | | |
195 | 0 | for entry in std::fs::read_dir(dir_path)? { |
196 | 0 | let entry = entry?; |
197 | 0 | let path = entry.path(); |
198 | | |
199 | 0 | let entry_results = process_directory_entry( |
200 | 0 | enforcer, |
201 | 0 | &path, |
202 | 0 | depth, |
203 | 0 | fail_fast, |
204 | 0 | verbose, |
205 | 0 | )?; |
206 | | |
207 | | // Handle fail-fast mode |
208 | 0 | if should_fail_fast(&entry_results, fail_fast) { |
209 | 0 | return Ok(entry_results); |
210 | 0 | } |
211 | | |
212 | 0 | results.extend(entry_results); |
213 | | } |
214 | | |
215 | 0 | Ok(results) |
216 | 0 | } |
217 | | |
218 | | /// Process a single directory entry (complexity: 6) |
219 | 0 | fn process_directory_entry( |
220 | 0 | enforcer: &QualityGateEnforcer, |
221 | 0 | path: &Path, |
222 | 0 | depth: AnalysisDepth, |
223 | 0 | fail_fast: bool, |
224 | 0 | verbose: bool, |
225 | 0 | ) -> Result<Vec<crate::quality::gates::GateResult>> { |
226 | 0 | if is_ruchy_file(path) { |
227 | 0 | process_ruchy_file(enforcer, path, depth, fail_fast, verbose) |
228 | 0 | } else if is_processable_directory(path) { |
229 | 0 | process_directory(enforcer, path, depth, fail_fast, verbose) |
230 | | } else { |
231 | 0 | Ok(Vec::new()) |
232 | | } |
233 | 0 | } |
234 | | |
235 | | /// Check if path is a Ruchy file (complexity: 2) |
236 | 0 | fn is_ruchy_file(path: &Path) -> bool { |
237 | 0 | path.is_file() && path.extension().is_some_and(|ext| ext == "ruchy") |
238 | 0 | } |
239 | | |
240 | | /// Check if directory should be processed (complexity: 3) |
241 | 0 | fn is_processable_directory(path: &Path) -> bool { |
242 | 0 | path.is_dir() && !path.file_name() |
243 | 0 | .unwrap_or_default() |
244 | 0 | .to_string_lossy() |
245 | 0 | .starts_with('.') |
246 | 0 | } |
247 | | |
248 | | /// Process a Ruchy file and handle errors (complexity: 5) |
249 | 0 | fn process_ruchy_file( |
250 | 0 | enforcer: &QualityGateEnforcer, |
251 | 0 | path: &Path, |
252 | 0 | depth: AnalysisDepth, |
253 | 0 | fail_fast: bool, |
254 | 0 | verbose: bool, |
255 | 0 | ) -> Result<Vec<crate::quality::gates::GateResult>> { |
256 | 0 | match process_file(enforcer, path, depth, verbose) { |
257 | 0 | Ok(result) => { |
258 | 0 | if fail_fast && !result.passed { |
259 | 0 | eprintln!("❌ Failed fast on {}", path.display()); |
260 | 0 | } |
261 | 0 | Ok(vec![result]) |
262 | | } |
263 | 0 | Err(e) => { |
264 | 0 | eprintln!("⚠️ Error processing {}: {}", path.display(), e); |
265 | 0 | if fail_fast { |
266 | 0 | Err(e) |
267 | | } else { |
268 | 0 | Ok(Vec::new()) |
269 | | } |
270 | | } |
271 | | } |
272 | 0 | } |
273 | | |
274 | | /// Check if we should fail fast (complexity: 2) |
275 | 0 | fn should_fail_fast(results: &[crate::quality::gates::GateResult], fail_fast: bool) -> bool { |
276 | 0 | fail_fast && results.iter().any(|r| !r.passed) |
277 | 0 | } |
278 | | |
279 | 0 | fn print_console_results(results: &[crate::quality::gates::GateResult], verbose: bool) -> Result<()> { |
280 | 0 | for (i, result) in results.iter().enumerate() { |
281 | 0 | println!("\n📋 Quality Gate #{}: {}", i + 1, if result.passed { "✅ PASSED" } else { "❌ FAILED" }); |
282 | 0 | println!(" Score: {:.1}% ({})", result.score * 100.0, result.grade); |
283 | 0 | println!(" Confidence: {:.1}%", result.confidence * 100.0); |
284 | | |
285 | 0 | if !result.violations.is_empty() { |
286 | 0 | println!(" Violations:"); |
287 | 0 | for violation in &result.violations { |
288 | 0 | println!(" • {}", violation.message); |
289 | 0 | if verbose { |
290 | 0 | println!(" Type: {:?}, Severity: {:?}", violation.violation_type, violation.severity); |
291 | 0 | println!(" Required: {:.3}, Actual: {:.3}", violation.required, violation.actual); |
292 | 0 | } |
293 | | } |
294 | 0 | } |
295 | | |
296 | 0 | if !result.gaming_warnings.is_empty() { |
297 | 0 | println!(" Warnings:"); |
298 | 0 | for warning in &result.gaming_warnings { |
299 | 0 | println!(" ⚠️ {warning}"); |
300 | 0 | } |
301 | 0 | } |
302 | | } |
303 | | |
304 | 0 | let passed = results.iter().filter(|r| r.passed).count(); |
305 | 0 | let total = results.len(); |
306 | | |
307 | 0 | println!("\n📊 Summary: {passed}/{total} gates passed"); |
308 | | |
309 | 0 | Ok(()) |
310 | 0 | } |
311 | | |
312 | 0 | fn print_json_results(results: &[crate::quality::gates::GateResult]) -> Result<()> { |
313 | 0 | let json = serde_json::to_string_pretty(results)?; |
314 | 0 | println!("{json}"); |
315 | 0 | Ok(()) |
316 | 0 | } |
317 | | |
318 | 0 | fn print_junit_results(results: &[crate::quality::gates::GateResult]) -> Result<()> { |
319 | 0 | let total = results.len(); |
320 | 0 | let failures = results.iter().filter(|r| !r.passed).count(); |
321 | | |
322 | 0 | println!(r#"<?xml version="1.0" encoding="UTF-8"?>"#); |
323 | 0 | println!(r#"<testsuite name="Quality Gates" tests="{total}" failures="{failures}" time="0.0">"#); |
324 | | |
325 | 0 | for (i, result) in results.iter().enumerate() { |
326 | 0 | let test_name = format!("quality-gate-{i}"); |
327 | 0 | if result.passed { |
328 | 0 | println!(r#" <testcase name="{test_name}" classname="QualityGate" time="0.0"/>"#); |
329 | 0 | } else { |
330 | 0 | println!(r#" <testcase name="{test_name}" classname="QualityGate" time="0.0">"#); |
331 | 0 | println!(r#" <failure message="Quality gate violation">Score: {:.1}%, Grade: {}</failure>"#, |
332 | 0 | result.score * 100.0, result.grade); |
333 | 0 | println!(r" </testcase>"); |
334 | 0 | } |
335 | | } |
336 | | |
337 | 0 | println!("</testsuite>"); |
338 | 0 | Ok(()) |
339 | 0 | } |
340 | | |
341 | | #[cfg(test)] |
342 | | mod tests { |
343 | | use super::*; |
344 | | use tempfile::TempDir; |
345 | | use std::fs; |
346 | | use crate::quality::gates::QualityGateConfig; |
347 | | |
348 | | fn create_test_ruchy_file(dir: &Path, filename: &str, content: &str) -> std::path::PathBuf { |
349 | | let file_path = dir.join(filename); |
350 | | fs::write(&file_path, content).unwrap(); |
351 | | file_path |
352 | | } |
353 | | |
354 | | fn create_test_project_structure(dir: &Path) -> std::path::PathBuf { |
355 | | // Create Cargo.toml to mark as project root |
356 | | fs::write(dir.join("Cargo.toml"), "[package]\nname = \"test\"\nversion = \"0.1.0\"").unwrap(); |
357 | | |
358 | | // Create .ruchy directory |
359 | | fs::create_dir_all(dir.join(".ruchy")).unwrap(); |
360 | | |
361 | | // Create some test Ruchy files |
362 | | create_test_ruchy_file(dir, "test.ruchy", "let x = 5\nprintln(x)"); |
363 | | create_test_ruchy_file(dir, "simple.ruchy", "println(\"hello\")"); |
364 | | |
365 | | dir.to_path_buf() |
366 | | } |
367 | | |
368 | | // Test 1: Project Root Finding |
369 | | #[test] |
370 | | fn test_find_project_root_with_cargo_toml() { |
371 | | let temp_dir = TempDir::new().unwrap(); |
372 | | let project_dir = temp_dir.path(); |
373 | | |
374 | | // Create Cargo.toml |
375 | | fs::write(project_dir.join("Cargo.toml"), "[package]").unwrap(); |
376 | | |
377 | | let found_root = find_project_root(project_dir).unwrap(); |
378 | | assert_eq!(found_root, project_dir); |
379 | | } |
380 | | |
381 | | #[test] |
382 | | fn test_find_project_root_with_ruchy_dir() { |
383 | | let temp_dir = TempDir::new().unwrap(); |
384 | | let project_dir = temp_dir.path(); |
385 | | |
386 | | // Create .ruchy directory |
387 | | fs::create_dir_all(project_dir.join(".ruchy")).unwrap(); |
388 | | |
389 | | let found_root = find_project_root(project_dir).unwrap(); |
390 | | assert_eq!(found_root, project_dir); |
391 | | } |
392 | | |
393 | | #[test] |
394 | | fn test_find_project_root_from_file() { |
395 | | let temp_dir = TempDir::new().unwrap(); |
396 | | let project_dir = temp_dir.path(); |
397 | | |
398 | | // Create Cargo.toml |
399 | | fs::write(project_dir.join("Cargo.toml"), "[package]").unwrap(); |
400 | | |
401 | | // Create a file in project |
402 | | let file_path = create_test_ruchy_file(project_dir, "test.ruchy", "let x = 5"); |
403 | | |
404 | | let found_root = find_project_root(&file_path).unwrap(); |
405 | | assert_eq!(found_root, project_dir); |
406 | | } |
407 | | |
408 | | #[test] |
409 | | fn test_find_project_root_nested() { |
410 | | let temp_dir = TempDir::new().unwrap(); |
411 | | let project_dir = temp_dir.path(); |
412 | | |
413 | | // Create nested directory structure |
414 | | let nested_dir = project_dir.join("src").join("deep"); |
415 | | fs::create_dir_all(&nested_dir).unwrap(); |
416 | | |
417 | | // Create Cargo.toml at root |
418 | | fs::write(project_dir.join("Cargo.toml"), "[package]").unwrap(); |
419 | | |
420 | | // Create file in nested directory |
421 | | let file_path = create_test_ruchy_file(&nested_dir, "nested.ruchy", "println(\"nested\")"); |
422 | | |
423 | | let found_root = find_project_root(&file_path).unwrap(); |
424 | | assert_eq!(found_root, project_dir); |
425 | | } |
426 | | |
427 | | #[test] |
428 | | fn test_find_project_root_fallback() { |
429 | | let temp_dir = TempDir::new().unwrap(); |
430 | | let some_dir = temp_dir.path().join("no_project_markers"); |
431 | | fs::create_dir_all(&some_dir).unwrap(); |
432 | | |
433 | | let found_root = find_project_root(&some_dir).unwrap(); |
434 | | // Should fallback to current directory |
435 | | assert_eq!(found_root, Path::new(".")); |
436 | | } |
437 | | |
438 | | // Test 2: CI Overrides |
439 | | #[test] |
440 | | fn test_apply_ci_overrides() { |
441 | | let mut config = QualityGateConfig::default(); |
442 | | |
443 | | // Set lower initial values to test overrides |
444 | | config.min_score = 0.6; |
445 | | config.component_thresholds.correctness = 0.7; |
446 | | config.component_thresholds.safety = 0.7; |
447 | | config.anti_gaming.min_confidence = 0.5; |
448 | | config.ci_integration.fail_on_violation = false; |
449 | | |
450 | | let ci_config = apply_ci_overrides(config); |
451 | | |
452 | | // Should apply stricter thresholds |
453 | | assert!(ci_config.min_score >= 0.8); |
454 | | assert!(ci_config.component_thresholds.correctness >= 0.9); |
455 | | assert!(ci_config.component_thresholds.safety >= 0.9); |
456 | | assert!(ci_config.anti_gaming.min_confidence >= 0.8); |
457 | | assert!(ci_config.ci_integration.fail_on_violation); |
458 | | } |
459 | | |
460 | | #[test] |
461 | | fn test_ci_overrides_preserve_higher_values() { |
462 | | let mut config = QualityGateConfig::default(); |
463 | | |
464 | | // Set higher initial values |
465 | | config.min_score = 0.95; |
466 | | config.component_thresholds.correctness = 0.95; |
467 | | config.component_thresholds.safety = 0.95; |
468 | | config.anti_gaming.min_confidence = 0.95; |
469 | | |
470 | | let ci_config = apply_ci_overrides(config); |
471 | | |
472 | | // Should preserve higher existing values |
473 | | assert_eq!(ci_config.min_score, 0.95); |
474 | | assert_eq!(ci_config.component_thresholds.correctness, 0.95); |
475 | | assert_eq!(ci_config.component_thresholds.safety, 0.95); |
476 | | assert_eq!(ci_config.anti_gaming.min_confidence, 0.95); |
477 | | } |
478 | | |
479 | | // Test 3: Analysis Depth Parsing |
480 | | #[test] |
481 | | fn test_analysis_depth_parsing() { |
482 | | let temp_dir = TempDir::new().unwrap(); |
483 | | let project_dir = create_test_project_structure(temp_dir.path()); |
484 | | let file_path = create_test_ruchy_file(&project_dir, "depth_test.ruchy", "let x = 1"); |
485 | | |
486 | | // Test all valid depth values |
487 | | let depths = vec![ |
488 | | ("shallow", true), |
489 | | ("standard", true), |
490 | | ("deep", true), |
491 | | ("invalid", false), |
492 | | ("", false), |
493 | | ]; |
494 | | |
495 | | for (depth_str, should_succeed) in depths { |
496 | | let result = enforce_quality_gates( |
497 | | &file_path, |
498 | | None, |
499 | | depth_str, |
500 | | false, |
501 | | "console", |
502 | | None, |
503 | | false, |
504 | | false, |
505 | | ); |
506 | | |
507 | | if should_succeed { |
508 | | // For valid depths, should not fail on depth parsing |
509 | | // May fail on other quality issues, but not depth parsing |
510 | | if let Err(e) = &result { |
511 | | assert!(!e.to_string().contains("Invalid depth"), |
512 | | "Should not fail on depth parsing for '{depth_str}'"); |
513 | | } |
514 | | } else { |
515 | | assert!(result.is_err(), "Should fail for invalid depth: '{depth_str}'"); |
516 | | if let Err(e) = result { |
517 | | assert!(e.to_string().contains("Invalid depth"), |
518 | | "Should fail with depth error for '{depth_str}'"); |
519 | | } |
520 | | } |
521 | | } |
522 | | } |
523 | | |
524 | | // Test 4: Format Validation |
525 | | #[test] |
526 | | fn test_format_validation() { |
527 | | let temp_dir = TempDir::new().unwrap(); |
528 | | let project_dir = create_test_project_structure(temp_dir.path()); |
529 | | let file_path = create_test_ruchy_file(&project_dir, "format_test.ruchy", "let x = 1"); |
530 | | |
531 | | let formats = vec![ |
532 | | ("console", true), |
533 | | ("json", true), |
534 | | ("junit", true), |
535 | | ("xml", false), |
536 | | ("invalid", false), |
537 | | ]; |
538 | | |
539 | | for (format, should_succeed) in formats { |
540 | | let result = enforce_quality_gates( |
541 | | &file_path, |
542 | | None, |
543 | | "standard", |
544 | | false, |
545 | | format, |
546 | | None, |
547 | | false, |
548 | | false, |
549 | | ); |
550 | | |
551 | | if should_succeed { |
552 | | // Valid formats shouldn't fail on format parsing |
553 | | if let Err(e) = &result { |
554 | | assert!(!e.to_string().contains("Invalid format"), |
555 | | "Should not fail on format parsing for '{format}'"); |
556 | | } |
557 | | } else { |
558 | | assert!(result.is_err(), "Should fail for invalid format: '{format}'"); |
559 | | if let Err(e) = result { |
560 | | assert!(e.to_string().contains("Invalid format"), |
561 | | "Should fail with format error for '{format}'"); |
562 | | } |
563 | | } |
564 | | } |
565 | | } |
566 | | |
567 | | // Test 5: File vs Directory Processing |
568 | | #[test] |
569 | | fn test_single_file_processing() { |
570 | | let temp_dir = TempDir::new().unwrap(); |
571 | | let project_dir = create_test_project_structure(temp_dir.path()); |
572 | | let file_path = create_test_ruchy_file(&project_dir, "single.ruchy", "println(\"test\")"); |
573 | | |
574 | | // This should not crash and should process the single file |
575 | | let result = enforce_quality_gates( |
576 | | &file_path, |
577 | | None, |
578 | | "standard", |
579 | | false, |
580 | | "console", |
581 | | None, |
582 | | false, |
583 | | false, |
584 | | ); |
585 | | |
586 | | // May fail due to quality issues, but should not crash |
587 | | assert!(result.is_ok() || result.is_err(), "Should complete processing"); |
588 | | } |
589 | | |
590 | | #[test] |
591 | | fn test_directory_processing() { |
592 | | let temp_dir = TempDir::new().unwrap(); |
593 | | let project_dir = create_test_project_structure(temp_dir.path()); |
594 | | |
595 | | // Create multiple files |
596 | | create_test_ruchy_file(&project_dir, "file1.ruchy", "let a = 1"); |
597 | | create_test_ruchy_file(&project_dir, "file2.ruchy", "let b = 2"); |
598 | | |
599 | | // This should process directory |
600 | | let result = enforce_quality_gates( |
601 | | &project_dir, |
602 | | None, |
603 | | "standard", |
604 | | false, |
605 | | "console", |
606 | | None, |
607 | | false, |
608 | | false, |
609 | | ); |
610 | | |
611 | | // May fail due to quality issues, but should not crash |
612 | | assert!(result.is_ok() || result.is_err(), "Should complete directory processing"); |
613 | | } |
614 | | |
615 | | #[test] |
616 | | fn test_nonexistent_path() { |
617 | | let temp_dir = TempDir::new().unwrap(); |
618 | | let nonexistent = temp_dir.path().join("does_not_exist.ruchy"); |
619 | | |
620 | | let result = enforce_quality_gates( |
621 | | &nonexistent, |
622 | | None, |
623 | | "standard", |
624 | | false, |
625 | | "console", |
626 | | None, |
627 | | false, |
628 | | false, |
629 | | ); |
630 | | |
631 | | assert!(result.is_err(), "Should fail for nonexistent path"); |
632 | | } |
633 | | |
634 | | // Test 6: Configuration Loading |
635 | | #[test] |
636 | | fn test_custom_config_loading() { |
637 | | let temp_dir = TempDir::new().unwrap(); |
638 | | let project_dir = create_test_project_structure(temp_dir.path()); |
639 | | let file_path = create_test_ruchy_file(&project_dir, "config_test.ruchy", "let x = 1"); |
640 | | |
641 | | // Create custom config |
642 | | let config_dir = temp_dir.path().join("custom_config"); |
643 | | fs::create_dir_all(&config_dir).unwrap(); |
644 | | fs::create_dir_all(config_dir.join(".ruchy")).unwrap(); |
645 | | |
646 | | // Create custom score.toml |
647 | | let config_content = r#" |
648 | | min_score = 0.5 |
649 | | min_grade = "D" |
650 | | |
651 | | [component_thresholds] |
652 | | correctness = 0.4 |
653 | | performance = 0.4 |
654 | | maintainability = 0.4 |
655 | | safety = 0.4 |
656 | | idiomaticity = 0.4 |
657 | | "#; |
658 | | fs::write(config_dir.join(".ruchy").join("score.toml"), config_content).unwrap(); |
659 | | |
660 | | let custom_config_path = config_dir.join("score.toml"); |
661 | | |
662 | | let result = enforce_quality_gates( |
663 | | &file_path, |
664 | | Some(&custom_config_path), |
665 | | "standard", |
666 | | false, |
667 | | "console", |
668 | | None, |
669 | | false, |
670 | | false, |
671 | | ); |
672 | | |
673 | | // Should use custom config (may pass due to lower thresholds) |
674 | | assert!(result.is_ok() || result.is_err(), "Should process with custom config"); |
675 | | } |
676 | | |
677 | | // Test 7: Export Functionality |
678 | | #[test] |
679 | | fn test_export_directory_creation() { |
680 | | let temp_dir = TempDir::new().unwrap(); |
681 | | let project_dir = create_test_project_structure(temp_dir.path()); |
682 | | let file_path = create_test_ruchy_file(&project_dir, "export_test.ruchy", "let x = 1"); |
683 | | let export_dir = temp_dir.path().join("exports"); |
684 | | |
685 | | let _result = enforce_quality_gates( |
686 | | &file_path, |
687 | | None, |
688 | | "standard", |
689 | | false, |
690 | | "console", |
691 | | Some(&export_dir), |
692 | | false, |
693 | | false, |
694 | | ); |
695 | | |
696 | | // Should create export directory |
697 | | assert!(export_dir.exists(), "Export directory should be created"); |
698 | | } |
699 | | |
700 | | // Test 8: Error Handling |
701 | | #[test] |
702 | | fn test_invalid_ruchy_syntax() { |
703 | | let temp_dir = TempDir::new().unwrap(); |
704 | | let project_dir = create_test_project_structure(temp_dir.path()); |
705 | | |
706 | | // Create file with invalid syntax |
707 | | let bad_file = create_test_ruchy_file(&project_dir, "bad_syntax.ruchy", "let = = invalid syntax here"); |
708 | | |
709 | | let result = enforce_quality_gates( |
710 | | &bad_file, |
711 | | None, |
712 | | "standard", |
713 | | false, |
714 | | "console", |
715 | | None, |
716 | | false, |
717 | | false, |
718 | | ); |
719 | | |
720 | | // Should handle parsing errors gracefully |
721 | | assert!(result.is_err(), "Should fail gracefully on invalid syntax"); |
722 | | } |
723 | | |
724 | | // Test 9: Verbose Output Mode |
725 | | #[test] |
726 | | fn test_verbose_mode_flag() { |
727 | | let temp_dir = TempDir::new().unwrap(); |
728 | | let project_dir = create_test_project_structure(temp_dir.path()); |
729 | | let file_path = create_test_ruchy_file(&project_dir, "verbose_test.ruchy", "let x = 1"); |
730 | | |
731 | | // Test both verbose modes (this mainly tests that verbose flag is accepted) |
732 | | for verbose in [true, false] { |
733 | | let result = enforce_quality_gates( |
734 | | &file_path, |
735 | | None, |
736 | | "standard", |
737 | | false, |
738 | | "console", |
739 | | None, |
740 | | false, |
741 | | verbose, |
742 | | ); |
743 | | |
744 | | // Should accept verbose flag without crashing |
745 | | assert!(result.is_ok() || result.is_err(), "Should handle verbose flag"); |
746 | | } |
747 | | } |
748 | | |
749 | | // Test 10: Fail Fast Mode |
750 | | #[test] |
751 | | fn test_fail_fast_mode() { |
752 | | let temp_dir = TempDir::new().unwrap(); |
753 | | let project_dir = create_test_project_structure(temp_dir.path()); |
754 | | |
755 | | // Create multiple files |
756 | | create_test_ruchy_file(&project_dir, "fail1.ruchy", "let a = 1"); |
757 | | create_test_ruchy_file(&project_dir, "fail2.ruchy", "let b = 2"); |
758 | | |
759 | | for fail_fast in [true, false] { |
760 | | let result = enforce_quality_gates( |
761 | | &project_dir, |
762 | | None, |
763 | | "standard", |
764 | | fail_fast, |
765 | | "console", |
766 | | None, |
767 | | false, |
768 | | false, |
769 | | ); |
770 | | |
771 | | // Should handle fail_fast flag without crashing |
772 | | assert!(result.is_ok() || result.is_err(), "Should handle fail_fast flag"); |
773 | | } |
774 | | } |
775 | | } |