Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}