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