ruchy/quality/
gates.rs

1//! Quality gate enforcement system (RUCHY-0815)
2use crate::quality::scoring::{Grade, QualityScore};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6/// Quality gate configuration
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct QualityGateConfig {
9    /// Minimum overall score required (0.0-1.0)
10    pub min_score: f64,
11    /// Minimum grade required
12    pub min_grade: Grade,
13    /// Component-specific thresholds
14    pub component_thresholds: ComponentThresholds,
15    /// Anti-gaming rules
16    pub anti_gaming: AntiGamingRules,
17    /// CI/CD integration settings
18    pub ci_integration: CiIntegration,
19    /// Project-specific overrides
20    pub project_overrides: HashMap<String, f64>,
21}
22/// Component-specific quality thresholds
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ComponentThresholds {
25    /// Minimum correctness score (0.0-1.0)
26    pub correctness: f64,
27    /// Minimum performance score (0.0-1.0)
28    pub performance: f64,
29    /// Minimum maintainability score (0.0-1.0)
30    pub maintainability: f64,
31    /// Minimum safety score (0.0-1.0)
32    pub safety: f64,
33    /// Minimum idiomaticity score (0.0-1.0)
34    pub idiomaticity: f64,
35}
36/// Anti-gaming rules to prevent score manipulation
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AntiGamingRules {
39    /// Minimum confidence level required (0.0-1.0)
40    pub min_confidence: f64,
41    /// Maximum cache hit rate allowed (0.0-1.0) - prevents stale analysis
42    pub max_cache_hit_rate: f64,
43    /// Require deep analysis for critical files
44    pub require_deep_analysis: Vec<String>,
45    /// Penalty for files that are too small (gaming by splitting)
46    pub min_file_size_bytes: usize,
47    /// Penalty for excessive test file ratios (gaming with trivial tests)
48    pub max_test_ratio: f64,
49}
50/// CI/CD integration configuration
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CiIntegration {
53    /// Fail CI/CD pipeline on gate failure
54    pub fail_on_violation: bool,
55    /// Export results in `JUnit` XML format
56    pub junit_xml: bool,
57    /// Export results in JSON format for tooling
58    pub json_output: bool,
59    /// Send notifications on quality degradation
60    pub notifications: NotificationConfig,
61    /// Block merge requests below threshold
62    pub block_merge: bool,
63}
64/// Notification configuration
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct NotificationConfig {
67    /// Enable Slack notifications
68    pub slack: bool,
69    /// Enable email notifications  
70    pub email: bool,
71    /// Webhook URL for custom notifications
72    pub webhook: Option<String>,
73}
74/// Quality gate enforcement result
75#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
76pub struct GateResult {
77    /// Whether the quality gate passed
78    pub passed: bool,
79    /// Overall score achieved
80    pub score: f64,
81    /// Grade achieved
82    pub grade: Grade,
83    /// Specific violations found
84    pub violations: Vec<Violation>,
85    /// Confidence in the result
86    pub confidence: f64,
87    /// Anti-gaming warnings
88    pub gaming_warnings: Vec<String>,
89}
90/// Specific quality gate violation
91#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
92pub struct Violation {
93    /// Type of violation
94    pub violation_type: ViolationType,
95    /// Actual value that caused violation
96    pub actual: f64,
97    /// Required threshold
98    pub required: f64,
99    /// Severity of the violation
100    pub severity: Severity,
101    /// Human-readable message
102    pub message: String,
103}
104/// Types of quality gate violations
105#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
106pub enum ViolationType {
107    OverallScore,
108    Grade,
109    Correctness,
110    Performance,
111    Maintainability,
112    Safety,
113    Idiomaticity,
114    Confidence,
115    Gaming,
116}
117/// Violation severity levels
118#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
119pub enum Severity {
120    Critical, // Must fix to pass
121    High,     // Should fix soon
122    Medium,   // Should improve
123    Low,      // Nice to improve
124}
125/// Quality gate enforcer
126pub struct QualityGateEnforcer {
127    config: QualityGateConfig,
128}
129impl Default for QualityGateConfig {
130    fn default() -> Self {
131        Self {
132            min_score: 0.7, // B- grade minimum
133            min_grade: Grade::BMinus,
134            component_thresholds: ComponentThresholds {
135                correctness: 0.8,     // High correctness required
136                performance: 0.6,     // Moderate performance required
137                maintainability: 0.7, // Good maintainability required
138                safety: 0.8,          // High safety required
139                idiomaticity: 0.5,    // Basic idiomaticity required
140            },
141            anti_gaming: AntiGamingRules {
142                min_confidence: 0.6,
143                max_cache_hit_rate: 0.8,
144                require_deep_analysis: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
145                min_file_size_bytes: 100,
146                max_test_ratio: 2.0,
147            },
148            ci_integration: CiIntegration {
149                fail_on_violation: true,
150                junit_xml: true,
151                json_output: true,
152                notifications: NotificationConfig {
153                    slack: false,
154                    email: false,
155                    webhook: None,
156                },
157                block_merge: true,
158            },
159            project_overrides: HashMap::new(),
160        }
161    }
162}
163impl QualityGateEnforcer {
164    /// # Examples
165    ///
166    /// ```
167    /// use ruchy::quality::gates::QualityGateEnforcer;
168    ///
169    /// let instance = QualityGateEnforcer::new();
170    /// // Verify behavior
171    /// ```
172    pub fn new(config: QualityGateConfig) -> Self {
173        Self { config }
174    }
175    /// Load configuration from .ruchy/score.toml
176    /// # Examples
177    ///
178    /// ```
179    /// use ruchy::quality::gates::QualityGateEnforcer;
180    ///
181    /// let mut instance = QualityGateEnforcer::new();
182    /// let result = instance.load_config();
183    /// // Verify behavior
184    /// ```
185    pub fn load_config(project_root: &Path) -> anyhow::Result<QualityGateConfig> {
186        let config_path = project_root.join(".ruchy").join("score.toml");
187        if config_path.exists() {
188            let content = std::fs::read_to_string(&config_path)?;
189            let config: QualityGateConfig = toml::from_str(&content)?;
190            Ok(config)
191        } else {
192            // Create default configuration file
193            let default_config = QualityGateConfig::default();
194            std::fs::create_dir_all(project_root.join(".ruchy"))?;
195            let toml_content = toml::to_string_pretty(&default_config)?;
196            std::fs::write(&config_path, toml_content)?;
197            Ok(default_config)
198        }
199    }
200    /// Enforce quality gates on a score
201    /// # Examples
202    ///
203    /// ```
204    /// use ruchy::quality::gates::QualityGateEnforcer;
205    ///
206    /// let mut instance = QualityGateEnforcer::new();
207    /// let result = instance.enforce_gates();
208    /// // Verify behavior
209    /// ```
210    pub fn enforce_gates(&self, score: &QualityScore, file_path: Option<&PathBuf>) -> GateResult {
211        let mut violations = Vec::new();
212        let mut gaming_warnings = Vec::new();
213        // Check overall score threshold
214        if score.value < self.config.min_score {
215            violations.push(Violation {
216                violation_type: ViolationType::OverallScore,
217                actual: score.value,
218                required: self.config.min_score,
219                severity: Severity::Critical,
220                message: format!(
221                    "Overall score {:.1}% below minimum {:.1}%",
222                    score.value * 100.0,
223                    self.config.min_score * 100.0
224                ),
225            });
226        }
227        // Check grade requirement
228        if score.grade < self.config.min_grade {
229            violations.push(Violation {
230                violation_type: ViolationType::Grade,
231                actual: score.value,
232                required: self.config.min_score,
233                severity: Severity::Critical,
234                message: format!(
235                    "Grade {} below minimum {}",
236                    score.grade, self.config.min_grade
237                ),
238            });
239        }
240        // Check component thresholds
241        self.check_component_thresholds(score, &mut violations);
242        // Check anti-gaming rules
243        self.check_anti_gaming_rules(score, file_path, &mut gaming_warnings, &mut violations);
244        // Check confidence threshold
245        if score.confidence < self.config.anti_gaming.min_confidence {
246            violations.push(Violation {
247                violation_type: ViolationType::Confidence,
248                actual: score.confidence,
249                required: self.config.anti_gaming.min_confidence,
250                severity: Severity::High,
251                message: format!(
252                    "Confidence {:.1}% below minimum {:.1}%",
253                    score.confidence * 100.0,
254                    self.config.anti_gaming.min_confidence * 100.0
255                ),
256            });
257        }
258        let passed = violations.iter().all(|v| v.severity != Severity::Critical);
259        GateResult {
260            passed,
261            score: score.value,
262            grade: score.grade,
263            violations,
264            confidence: score.confidence,
265            gaming_warnings,
266        }
267    }
268    fn check_component_thresholds(&self, score: &QualityScore, violations: &mut Vec<Violation>) {
269        let thresholds = &self.config.component_thresholds;
270        if score.components.correctness < thresholds.correctness {
271            violations.push(Violation {
272                violation_type: ViolationType::Correctness,
273                actual: score.components.correctness,
274                required: thresholds.correctness,
275                severity: Severity::Critical,
276                message: format!(
277                    "Correctness {:.1}% below minimum {:.1}%",
278                    score.components.correctness * 100.0,
279                    thresholds.correctness * 100.0
280                ),
281            });
282        }
283        if score.components.performance < thresholds.performance {
284            violations.push(Violation {
285                violation_type: ViolationType::Performance,
286                actual: score.components.performance,
287                required: thresholds.performance,
288                severity: Severity::High,
289                message: format!(
290                    "Performance {:.1}% below minimum {:.1}%",
291                    score.components.performance * 100.0,
292                    thresholds.performance * 100.0
293                ),
294            });
295        }
296        if score.components.maintainability < thresholds.maintainability {
297            violations.push(Violation {
298                violation_type: ViolationType::Maintainability,
299                actual: score.components.maintainability,
300                required: thresholds.maintainability,
301                severity: Severity::High,
302                message: format!(
303                    "Maintainability {:.1}% below minimum {:.1}%",
304                    score.components.maintainability * 100.0,
305                    thresholds.maintainability * 100.0
306                ),
307            });
308        }
309        if score.components.safety < thresholds.safety {
310            violations.push(Violation {
311                violation_type: ViolationType::Safety,
312                actual: score.components.safety,
313                required: thresholds.safety,
314                severity: Severity::Critical,
315                message: format!(
316                    "Safety {:.1}% below minimum {:.1}%",
317                    score.components.safety * 100.0,
318                    thresholds.safety * 100.0
319                ),
320            });
321        }
322        if score.components.idiomaticity < thresholds.idiomaticity {
323            violations.push(Violation {
324                violation_type: ViolationType::Idiomaticity,
325                actual: score.components.idiomaticity,
326                required: thresholds.idiomaticity,
327                severity: Severity::Medium,
328                message: format!(
329                    "Idiomaticity {:.1}% below minimum {:.1}%",
330                    score.components.idiomaticity * 100.0,
331                    thresholds.idiomaticity * 100.0
332                ),
333            });
334        }
335    }
336    fn check_anti_gaming_rules(
337        &self,
338        score: &QualityScore,
339        file_path: Option<&PathBuf>,
340        gaming_warnings: &mut Vec<String>,
341        violations: &mut Vec<Violation>,
342    ) {
343        // Check cache hit rate (prevent stale analysis gaming)
344        if score.cache_hit_rate > self.config.anti_gaming.max_cache_hit_rate {
345            gaming_warnings.push(format!(
346                "High cache hit rate {:.1}% may indicate stale analysis",
347                score.cache_hit_rate * 100.0
348            ));
349        }
350        // Check file size requirements
351        if let Some(path) = file_path {
352            if let Ok(metadata) = std::fs::metadata(path) {
353                if metadata.len() < self.config.anti_gaming.min_file_size_bytes as u64 {
354                    gaming_warnings.push(format!(
355                        "File {} is very small ({} bytes) - may indicate gaming by splitting",
356                        path.display(),
357                        metadata.len()
358                    ));
359                }
360            }
361            // Check if critical files require deep analysis
362            let path_str = path.to_string_lossy();
363            if self
364                .config
365                .anti_gaming
366                .require_deep_analysis
367                .iter()
368                .any(|p| path_str.contains(p))
369                && score.confidence < 0.9
370            {
371                violations.push(Violation {
372                    violation_type: ViolationType::Gaming,
373                    actual: score.confidence,
374                    required: 0.9,
375                    severity: Severity::Critical,
376                    message: format!(
377                        "Critical file {} requires deep analysis (confidence < 90%)",
378                        path.display()
379                    ),
380                });
381            }
382        }
383    }
384    /// Export results for CI/CD integration
385    /// # Examples
386    ///
387    /// ```ignore
388    /// use ruchy::quality::gates::export_ci_results;
389    ///
390    /// let result = export_ci_results(());
391    /// assert_eq!(result, Ok(()));
392    /// ```
393    pub fn export_ci_results(
394        &self,
395        results: &[GateResult],
396        output_dir: &Path,
397    ) -> anyhow::Result<()> {
398        if self.config.ci_integration.json_output {
399            self.export_json_results(results, output_dir)?;
400        }
401        if self.config.ci_integration.junit_xml {
402            self.export_junit_results(results, output_dir)?;
403        }
404        Ok(())
405    }
406    fn export_json_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
407        let output_path = output_dir.join("quality-gates.json");
408        let json_content = serde_json::to_string_pretty(results)?;
409        std::fs::write(output_path, json_content)?;
410        Ok(())
411    }
412    fn export_junit_results(
413        &self,
414        results: &[GateResult],
415        output_dir: &Path,
416    ) -> anyhow::Result<()> {
417        let output_path = output_dir.join("quality-gates.xml");
418        let total = results.len();
419        let failures = results.iter().filter(|r| !r.passed).count();
420        let mut xml = format!(
421            r#"<?xml version="1.0" encoding="UTF-8"?>
422<testsuite name="Quality Gates" tests="{total}" failures="{failures}" time="0.0">
423"#
424        );
425        for (i, result) in results.iter().enumerate() {
426            let test_name = format!("quality-gate-{i}");
427            if result.passed {
428                xml.push_str(&format!(
429                    r#"  <testcase name="{test_name}" classname="QualityGate" time="0.0"/>
430"#
431                ));
432            } else {
433                xml.push_str(&format!(
434                    r#"  <testcase name="{}" classname="QualityGate" time="0.0">
435    <failure message="Quality gate violation">Score: {:.1}%, Grade: {}</failure>
436  </testcase>
437"#,
438                    test_name,
439                    result.score * 100.0,
440                    result.grade
441                ));
442            }
443        }
444        xml.push_str("</testsuite>\n");
445        std::fs::write(output_path, xml)?;
446        Ok(())
447    }
448}
449impl PartialOrd for Grade {
450    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
451        Some(self.cmp(other))
452    }
453}
454impl Ord for Grade {
455    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
456        self.to_rank().cmp(&other.to_rank())
457    }
458}
459// PartialEq and Eq are now derived in scoring.rs
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use crate::quality::scoring::{Grade, QualityScore};
464    use tempfile::TempDir;
465    fn create_minimal_score() -> QualityScore {
466        use crate::quality::scoring::ScoreComponents;
467        QualityScore {
468            value: 0.5,
469            components: ScoreComponents {
470                correctness: 0.5,
471                performance: 0.5,
472                maintainability: 0.5,
473                safety: 0.5,
474                idiomaticity: 0.5,
475            },
476            grade: Grade::D,
477            confidence: 0.4,
478            cache_hit_rate: 0.3,
479        }
480    }
481    fn create_passing_score() -> QualityScore {
482        use crate::quality::scoring::ScoreComponents;
483        QualityScore {
484            value: 0.85,
485            components: ScoreComponents {
486                correctness: 0.9,
487                performance: 0.8,
488                maintainability: 0.8,
489                safety: 0.9,
490                idiomaticity: 0.7,
491            },
492            grade: Grade::APlus,
493            confidence: 0.9,
494            cache_hit_rate: 0.2,
495        }
496    }
497    // Test 1: Default Configuration Creation
498    #[test]
499    fn test_default_quality_gate_config() {
500        let config = QualityGateConfig::default();
501        assert_eq!(config.min_score, 0.7);
502        assert_eq!(config.min_grade, Grade::BMinus);
503        assert_eq!(config.component_thresholds.correctness, 0.8);
504        assert_eq!(config.component_thresholds.safety, 0.8);
505        assert_eq!(config.anti_gaming.min_confidence, 0.6);
506        assert!(config.ci_integration.fail_on_violation);
507        assert!(config.project_overrides.is_empty());
508    }
509    // Test 2: Quality Gate Enforcer Creation
510    #[test]
511    fn test_quality_gate_enforcer_creation() {
512        let config = QualityGateConfig::default();
513        let enforcer = QualityGateEnforcer::new(config);
514        // Verify enforcer uses the provided config
515        let score = create_minimal_score();
516        let result = enforcer.enforce_gates(&score, None);
517        // Should fail with default thresholds
518        assert!(!result.passed);
519        assert!(!result.violations.is_empty());
520    }
521    // Test 3: Passing Quality Gate - All Criteria Met
522    #[test]
523    fn test_quality_gate_passes_with_high_score() {
524        let config = QualityGateConfig::default();
525        let enforcer = QualityGateEnforcer::new(config);
526        let score = create_passing_score();
527        let result = enforcer.enforce_gates(&score, None);
528        assert!(result.passed, "High quality score should pass all gates");
529        assert_eq!(result.score, 0.85);
530        assert_eq!(result.grade, Grade::APlus);
531        assert!(result.violations.is_empty());
532        assert_eq!(result.confidence, 0.9);
533        assert!(result.gaming_warnings.is_empty());
534    }
535    // Test 4: Failing Overall Score Threshold
536    #[test]
537    fn test_quality_gate_fails_overall_score() {
538        let config = QualityGateConfig::default(); // min_score: 0.7
539        let enforcer = QualityGateEnforcer::new(config);
540        let mut score = create_minimal_score();
541        score.value = 0.6; // Below 0.7 threshold
542        let result = enforcer.enforce_gates(&score, None);
543        assert!(!result.passed, "Score below threshold should fail");
544        // Should have overall score violation
545        let overall_violations: Vec<_> = result
546            .violations
547            .iter()
548            .filter(|v| v.violation_type == ViolationType::OverallScore)
549            .collect();
550        assert_eq!(overall_violations.len(), 1);
551        let violation = &overall_violations[0];
552        assert_eq!(violation.actual, 0.6);
553        assert_eq!(violation.required, 0.7);
554        assert_eq!(violation.severity, Severity::Critical);
555        assert!(violation.message.contains("60.0%"));
556        assert!(violation.message.contains("70.0%"));
557    }
558    // Test 5: Confidence Threshold Violation
559    #[test]
560    fn test_confidence_threshold_violation() {
561        let config = QualityGateConfig::default(); // min_confidence: 0.6
562        let enforcer = QualityGateEnforcer::new(config);
563        let mut score = create_passing_score();
564        score.confidence = 0.4; // Below 0.6 threshold
565        let result = enforcer.enforce_gates(&score, None);
566        let confidence_violations: Vec<_> = result
567            .violations
568            .iter()
569            .filter(|v| v.violation_type == ViolationType::Confidence)
570            .collect();
571        assert_eq!(confidence_violations.len(), 1);
572        let violation = &confidence_violations[0];
573        assert_eq!(violation.severity, Severity::High);
574        assert_eq!(violation.actual, 0.4);
575        assert_eq!(violation.required, 0.6);
576    }
577    // Test 6: Configuration File Loading (Success)
578    #[test]
579    fn test_load_config_creates_default() {
580        let temp_dir = TempDir::new().unwrap();
581        let project_root = temp_dir.path();
582        let config = QualityGateEnforcer::load_config(project_root).unwrap();
583        // Should create default config
584        assert_eq!(config.min_score, 0.7);
585        assert_eq!(config.min_grade, Grade::BMinus);
586        // Should create .ruchy/score.toml file
587        let config_path = project_root.join(".ruchy").join("score.toml");
588        assert!(config_path.exists(), "Config file should be created");
589        // File should contain valid TOML
590        let content = std::fs::read_to_string(config_path).unwrap();
591        assert!(content.contains("min_score"));
592        assert!(content.contains("0.7"));
593    }
594    // Test 7: Serialization/Deserialization
595    #[test]
596    fn test_config_serialization() {
597        let original_config = QualityGateConfig::default();
598        // Serialize to TOML
599        let toml_content = toml::to_string(&original_config).unwrap();
600        assert!(toml_content.contains("min_score"));
601        // Deserialize back
602        let deserialized_config: QualityGateConfig = toml::from_str(&toml_content).unwrap();
603        assert_eq!(deserialized_config.min_score, original_config.min_score);
604        assert_eq!(deserialized_config.min_grade, original_config.min_grade);
605    }
606
607    // Test 8: Grade Comparison Testing
608    #[test]
609    fn test_grade_ordering() {
610        // Test all grade comparisons
611        assert!(Grade::F < Grade::D);
612        assert!(Grade::D < Grade::CMinus);
613        assert!(Grade::CMinus < Grade::C);
614        assert!(Grade::C < Grade::CPlus);
615        assert!(Grade::CPlus < Grade::BMinus);
616        assert!(Grade::BMinus < Grade::B);
617        assert!(Grade::B < Grade::BPlus);
618        assert!(Grade::BPlus < Grade::AMinus);
619        assert!(Grade::AMinus < Grade::A);
620        assert!(Grade::A < Grade::APlus);
621
622        // Test specific comparisons used in gates
623        assert!(Grade::C < Grade::BMinus); // Used in test_grade_threshold_violation
624        assert!(Grade::BMinus < Grade::A);
625    }
626
627    // Test 9: Grade Threshold Violation
628    #[test]
629    fn test_grade_threshold_violation() {
630        let config = QualityGateConfig::default(); // min_grade: BMinus
631        let enforcer = QualityGateEnforcer::new(config);
632
633        let mut score = create_passing_score();
634        score.grade = Grade::C; // Below BMinus threshold
635
636        let result = enforcer.enforce_gates(&score, None);
637        let grade_violations: Vec<_> = result
638            .violations
639            .iter()
640            .filter(|v| v.violation_type == ViolationType::Grade)
641            .collect();
642
643        assert_eq!(grade_violations.len(), 1);
644        let violation = &grade_violations[0];
645        assert_eq!(violation.severity, Severity::Critical);
646        assert!(violation.message.contains("Grade C below minimum B-"));
647    }
648
649    // Test 10: Component Threshold Violations - Correctness
650    #[test]
651    fn test_correctness_threshold_violation() {
652        let config = QualityGateConfig::default(); // correctness: 0.8
653        let enforcer = QualityGateEnforcer::new(config);
654
655        let mut score = create_passing_score();
656        score.components.correctness = 0.7; // Below 0.8 threshold
657
658        let result = enforcer.enforce_gates(&score, None);
659        let correctness_violations: Vec<_> = result
660            .violations
661            .iter()
662            .filter(|v| v.violation_type == ViolationType::Correctness)
663            .collect();
664
665        assert_eq!(correctness_violations.len(), 1);
666        let violation = &correctness_violations[0];
667        assert_eq!(violation.actual, 0.7);
668        assert_eq!(violation.required, 0.8);
669        assert_eq!(violation.severity, Severity::Critical);
670        assert!(violation.message.contains("70.0%"));
671        assert!(violation.message.contains("80.0%"));
672    }
673
674    // Test 11: Component Threshold Violations - Performance
675    #[test]
676    fn test_performance_threshold_violation() {
677        let config = QualityGateConfig::default(); // performance: 0.6
678        let enforcer = QualityGateEnforcer::new(config);
679
680        let mut score = create_passing_score();
681        score.components.performance = 0.5; // Below 0.6 threshold
682
683        let result = enforcer.enforce_gates(&score, None);
684        let performance_violations: Vec<_> = result
685            .violations
686            .iter()
687            .filter(|v| v.violation_type == ViolationType::Performance)
688            .collect();
689
690        assert_eq!(performance_violations.len(), 1);
691        let violation = &performance_violations[0];
692        assert_eq!(violation.actual, 0.5);
693        assert_eq!(violation.required, 0.6);
694        assert_eq!(violation.severity, Severity::High);
695    }
696
697    // Test 12: Component Threshold Violations - Safety
698    #[test]
699    fn test_safety_threshold_violation() {
700        let config = QualityGateConfig::default(); // safety: 0.8
701        let enforcer = QualityGateEnforcer::new(config);
702
703        let mut score = create_passing_score();
704        score.components.safety = 0.75; // Below 0.8 threshold
705
706        let result = enforcer.enforce_gates(&score, None);
707        let safety_violations: Vec<_> = result
708            .violations
709            .iter()
710            .filter(|v| v.violation_type == ViolationType::Safety)
711            .collect();
712
713        assert_eq!(safety_violations.len(), 1);
714        let violation = &safety_violations[0];
715        assert_eq!(violation.severity, Severity::Critical);
716        assert!(violation.message.contains("75.0%"));
717        assert!(violation.message.contains("80.0%"));
718    }
719
720    // Test 13: Component Threshold Violations - Maintainability
721    #[test]
722    fn test_maintainability_threshold_violation() {
723        let config = QualityGateConfig::default(); // maintainability: 0.7
724        let enforcer = QualityGateEnforcer::new(config);
725
726        let mut score = create_passing_score();
727        score.components.maintainability = 0.65; // Below 0.7 threshold
728
729        let result = enforcer.enforce_gates(&score, None);
730        let maintainability_violations: Vec<_> = result
731            .violations
732            .iter()
733            .filter(|v| v.violation_type == ViolationType::Maintainability)
734            .collect();
735
736        assert_eq!(maintainability_violations.len(), 1);
737        let violation = &maintainability_violations[0];
738        assert_eq!(violation.severity, Severity::High);
739        assert_eq!(violation.actual, 0.65);
740        assert_eq!(violation.required, 0.7);
741    }
742
743    // Test 14: Component Threshold Violations - Idiomaticity
744    #[test]
745    fn test_idiomaticity_threshold_violation() {
746        let config = QualityGateConfig::default(); // idiomaticity: 0.5
747        let enforcer = QualityGateEnforcer::new(config);
748
749        let mut score = create_passing_score();
750        score.components.idiomaticity = 0.4; // Below 0.5 threshold
751
752        let result = enforcer.enforce_gates(&score, None);
753        let idiomaticity_violations: Vec<_> = result
754            .violations
755            .iter()
756            .filter(|v| v.violation_type == ViolationType::Idiomaticity)
757            .collect();
758
759        assert_eq!(idiomaticity_violations.len(), 1);
760        let violation = &idiomaticity_violations[0];
761        assert_eq!(violation.severity, Severity::Medium);
762        assert_eq!(violation.actual, 0.4);
763        assert_eq!(violation.required, 0.5);
764    }
765
766    // Test 15: Anti-Gaming Rules - Cache Hit Rate Warning
767    #[test]
768    fn test_high_cache_hit_rate_warning() {
769        let config = QualityGateConfig::default(); // max_cache_hit_rate: 0.8
770        let enforcer = QualityGateEnforcer::new(config);
771
772        let mut score = create_passing_score();
773        score.cache_hit_rate = 0.9; // Above 0.8 threshold
774
775        let result = enforcer.enforce_gates(&score, None);
776
777        assert!(!result.gaming_warnings.is_empty());
778        let warning = &result.gaming_warnings[0];
779        assert!(warning.contains("High cache hit rate 90.0%"));
780        assert!(warning.contains("stale analysis"));
781    }
782
783    // Test 16: Anti-Gaming Rules - File Size Warning
784    #[test]
785    fn test_small_file_size_warning() -> anyhow::Result<()> {
786        let temp_dir = TempDir::new().unwrap();
787        let small_file = temp_dir.path().join("small.rs");
788        std::fs::write(&small_file, "// Small file")?; // ~13 bytes, below 100 threshold
789
790        let config = QualityGateConfig::default(); // min_file_size_bytes: 100
791        let enforcer = QualityGateEnforcer::new(config);
792        let score = create_passing_score();
793
794        let result = enforcer.enforce_gates(&score, Some(&small_file));
795
796        assert!(!result.gaming_warnings.is_empty());
797        let warning = &result.gaming_warnings[0];
798        assert!(warning.contains("very small"));
799        assert!(warning.contains("gaming by splitting"));
800        Ok(())
801    }
802
803    // Test 17: Anti-Gaming Rules - Critical Files Deep Analysis
804    #[test]
805    fn test_critical_files_deep_analysis() {
806        let temp_dir = TempDir::new().unwrap();
807        let critical_file = temp_dir.path().join("src").join("main.rs");
808        std::fs::create_dir_all(critical_file.parent().unwrap()).unwrap();
809        std::fs::write(&critical_file, "fn main() {}").unwrap();
810
811        let config = QualityGateConfig::default(); // require_deep_analysis includes "src/main.rs"
812        let enforcer = QualityGateEnforcer::new(config);
813
814        let mut score = create_passing_score();
815        score.confidence = 0.8; // Below 0.9 required for critical files
816
817        let result = enforcer.enforce_gates(&score, Some(&critical_file));
818
819        let gaming_violations: Vec<_> = result
820            .violations
821            .iter()
822            .filter(|v| v.violation_type == ViolationType::Gaming)
823            .collect();
824
825        assert_eq!(gaming_violations.len(), 1);
826        let violation = &gaming_violations[0];
827        assert_eq!(violation.severity, Severity::Critical);
828        assert_eq!(violation.actual, 0.8);
829        assert_eq!(violation.required, 0.9);
830        assert!(violation.message.contains("deep analysis"));
831    }
832
833    // Test 18: Multiple Violations Combination
834    #[test]
835    fn test_multiple_violations() {
836        let config = QualityGateConfig::default();
837        let enforcer = QualityGateEnforcer::new(config);
838
839        let score = create_minimal_score(); // This should fail multiple criteria
840        let result = enforcer.enforce_gates(&score, None);
841
842        assert!(!result.passed);
843        // Should have multiple violations
844        assert!(result.violations.len() >= 3); // At least overall score, grade, and confidence
845
846        // Check we have different violation types
847        let violation_types: std::collections::HashSet<_> = result
848            .violations
849            .iter()
850            .map(|v| &v.violation_type)
851            .collect();
852        assert!(violation_types.contains(&ViolationType::OverallScore));
853        assert!(violation_types.contains(&ViolationType::Grade));
854        assert!(violation_types.contains(&ViolationType::Confidence));
855    }
856
857    // Test 19: CI Results Export - JSON Format
858    #[test]
859    fn test_export_json_results() -> anyhow::Result<()> {
860        let temp_dir = TempDir::new().unwrap();
861        let output_dir = temp_dir.path();
862
863        let mut config = QualityGateConfig::default();
864        config.ci_integration.json_output = true;
865        config.ci_integration.junit_xml = false;
866
867        let enforcer = QualityGateEnforcer::new(config);
868        let results = vec![create_gate_result_passed(), create_gate_result_failed()];
869
870        enforcer.export_ci_results(&results, output_dir)?;
871
872        let json_file = output_dir.join("quality-gates.json");
873        assert!(json_file.exists());
874
875        let content = std::fs::read_to_string(json_file)?;
876        let parsed: Vec<GateResult> = serde_json::from_str(&content)?;
877        assert_eq!(parsed.len(), 2);
878        assert!(parsed[0].passed);
879        assert!(!parsed[1].passed);
880
881        Ok(())
882    }
883
884    // Test 20: CI Results Export - JUnit XML Format
885    #[test]
886    fn test_export_junit_xml_results() -> anyhow::Result<()> {
887        let temp_dir = TempDir::new().unwrap();
888        let output_dir = temp_dir.path();
889
890        let mut config = QualityGateConfig::default();
891        config.ci_integration.json_output = false;
892        config.ci_integration.junit_xml = true;
893
894        let enforcer = QualityGateEnforcer::new(config);
895        let results = vec![create_gate_result_passed(), create_gate_result_failed()];
896
897        enforcer.export_ci_results(&results, output_dir)?;
898
899        let xml_file = output_dir.join("quality-gates.xml");
900        assert!(xml_file.exists());
901
902        let content = std::fs::read_to_string(xml_file)?;
903        assert!(content.contains("<?xml version="));
904        assert!(content.contains("<testsuite name=\"Quality Gates\" tests=\"2\" failures=\"1\""));
905        assert!(content.contains("<testcase name=\"quality-gate-0\" classname=\"QualityGate\""));
906        assert!(content.contains("<failure message=\"Quality gate violation\""));
907        assert!(content.contains("</testsuite>"));
908
909        Ok(())
910    }
911
912    // Test 21: Violation Type and Severity Enum Coverage
913    #[test]
914    fn test_violation_enums_coverage() {
915        // Test all ViolationType variants can be created and compared
916        let types = vec![
917            ViolationType::OverallScore,
918            ViolationType::Grade,
919            ViolationType::Correctness,
920            ViolationType::Performance,
921            ViolationType::Maintainability,
922            ViolationType::Safety,
923            ViolationType::Idiomaticity,
924            ViolationType::Confidence,
925            ViolationType::Gaming,
926        ];
927
928        for (i, vtype) in types.iter().enumerate() {
929            for (j, other) in types.iter().enumerate() {
930                if i == j {
931                    assert_eq!(vtype, other);
932                } else {
933                    assert_ne!(vtype, other);
934                }
935            }
936        }
937
938        // Test all Severity variants
939        let severities = [
940            Severity::Critical,
941            Severity::High,
942            Severity::Medium,
943            Severity::Low,
944        ];
945
946        for (i, severity) in severities.iter().enumerate() {
947            for (j, other) in severities.iter().enumerate() {
948                if i == j {
949                    assert_eq!(severity, other);
950                } else {
951                    assert_ne!(severity, other);
952                }
953            }
954        }
955    }
956
957    // Test 22: Notification Config Serialization
958    #[test]
959    fn test_notification_config_serialization() {
960        let config = NotificationConfig {
961            slack: true,
962            email: false,
963            webhook: Some("https://test.example.com/webhook".to_string()),
964        };
965
966        let serialized = serde_json::to_string(&config).unwrap();
967        let deserialized: NotificationConfig = serde_json::from_str(&serialized).unwrap();
968
969        assert!(deserialized.slack);
970        assert!(!deserialized.email);
971        assert_eq!(
972            deserialized.webhook,
973            Some("https://test.example.com/webhook".to_string())
974        );
975    }
976
977    // Helper functions for testing
978    fn create_gate_result_passed() -> GateResult {
979        GateResult {
980            passed: true,
981            score: 0.85,
982            grade: Grade::APlus,
983            violations: vec![],
984            confidence: 0.9,
985            gaming_warnings: vec![],
986        }
987    }
988
989    fn create_gate_result_failed() -> GateResult {
990        GateResult {
991            passed: false,
992            score: 0.6,
993            grade: Grade::D,
994            violations: vec![Violation {
995                violation_type: ViolationType::OverallScore,
996                actual: 0.6,
997                required: 0.7,
998                severity: Severity::Critical,
999                message: "Overall score 60.0% below minimum 70.0%".to_string(),
1000            }],
1001            confidence: 0.5,
1002            gaming_warnings: vec!["Low confidence warning".to_string()],
1003        }
1004    }
1005}
1006#[cfg(test)]
1007mod property_tests_gates {
1008    use proptest::proptest;
1009
1010    proptest! {
1011        /// Property: Function never panics on any input
1012        #[test]
1013        fn test_new_never_panics(input: String) {
1014            // Limit input size to avoid timeout
1015            let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
1016            // Function should not panic on any input
1017            let _ = std::panic::catch_unwind(|| {
1018                // Call function with various inputs
1019                // This is a template - adjust based on actual function signature
1020            });
1021        }
1022    }
1023}