1use crate::quality::scoring::{Grade, QualityScore};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct QualityGateConfig {
9 pub min_score: f64,
11 pub min_grade: Grade,
13 pub component_thresholds: ComponentThresholds,
15 pub anti_gaming: AntiGamingRules,
17 pub ci_integration: CiIntegration,
19 pub project_overrides: HashMap<String, f64>,
21}
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ComponentThresholds {
25 pub correctness: f64,
27 pub performance: f64,
29 pub maintainability: f64,
31 pub safety: f64,
33 pub idiomaticity: f64,
35}
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AntiGamingRules {
39 pub min_confidence: f64,
41 pub max_cache_hit_rate: f64,
43 pub require_deep_analysis: Vec<String>,
45 pub min_file_size_bytes: usize,
47 pub max_test_ratio: f64,
49}
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CiIntegration {
53 pub fail_on_violation: bool,
55 pub junit_xml: bool,
57 pub json_output: bool,
59 pub notifications: NotificationConfig,
61 pub block_merge: bool,
63}
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct NotificationConfig {
67 pub slack: bool,
69 pub email: bool,
71 pub webhook: Option<String>,
73}
74#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
76pub struct GateResult {
77 pub passed: bool,
79 pub score: f64,
81 pub grade: Grade,
83 pub violations: Vec<Violation>,
85 pub confidence: f64,
87 pub gaming_warnings: Vec<String>,
89}
90#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
92pub struct Violation {
93 pub violation_type: ViolationType,
95 pub actual: f64,
97 pub required: f64,
99 pub severity: Severity,
101 pub message: String,
103}
104#[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#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
119pub enum Severity {
120 Critical, High, Medium, Low, }
125pub struct QualityGateEnforcer {
127 config: QualityGateConfig,
128}
129impl Default for QualityGateConfig {
130 fn default() -> Self {
131 Self {
132 min_score: 0.7, min_grade: Grade::BMinus,
134 component_thresholds: ComponentThresholds {
135 correctness: 0.8, performance: 0.6, maintainability: 0.7, safety: 0.8, idiomaticity: 0.5, },
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 pub fn new(config: QualityGateConfig) -> Self {
173 Self { config }
174 }
175 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 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 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 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 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 self.check_component_thresholds(score, &mut violations);
242 self.check_anti_gaming_rules(score, file_path, &mut gaming_warnings, &mut violations);
244 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 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 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 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 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#[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]
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]
511 fn test_quality_gate_enforcer_creation() {
512 let config = QualityGateConfig::default();
513 let enforcer = QualityGateEnforcer::new(config);
514 let score = create_minimal_score();
516 let result = enforcer.enforce_gates(&score, None);
517 assert!(!result.passed);
519 assert!(!result.violations.is_empty());
520 }
521 #[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]
537 fn test_quality_gate_fails_overall_score() {
538 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
540 let mut score = create_minimal_score();
541 score.value = 0.6; let result = enforcer.enforce_gates(&score, None);
543 assert!(!result.passed, "Score below threshold should fail");
544 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]
560 fn test_confidence_threshold_violation() {
561 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
563 let mut score = create_passing_score();
564 score.confidence = 0.4; 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]
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 assert_eq!(config.min_score, 0.7);
585 assert_eq!(config.min_grade, Grade::BMinus);
586 let config_path = project_root.join(".ruchy").join("score.toml");
588 assert!(config_path.exists(), "Config file should be created");
589 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]
596 fn test_config_serialization() {
597 let original_config = QualityGateConfig::default();
598 let toml_content = toml::to_string(&original_config).unwrap();
600 assert!(toml_content.contains("min_score"));
601 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]
609 fn test_grade_ordering() {
610 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 assert!(Grade::C < Grade::BMinus); assert!(Grade::BMinus < Grade::A);
625 }
626
627 #[test]
629 fn test_grade_threshold_violation() {
630 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
632
633 let mut score = create_passing_score();
634 score.grade = Grade::C; 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]
651 fn test_correctness_threshold_violation() {
652 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
654
655 let mut score = create_passing_score();
656 score.components.correctness = 0.7; 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]
676 fn test_performance_threshold_violation() {
677 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
679
680 let mut score = create_passing_score();
681 score.components.performance = 0.5; 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]
699 fn test_safety_threshold_violation() {
700 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
702
703 let mut score = create_passing_score();
704 score.components.safety = 0.75; 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]
722 fn test_maintainability_threshold_violation() {
723 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
725
726 let mut score = create_passing_score();
727 score.components.maintainability = 0.65; 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]
745 fn test_idiomaticity_threshold_violation() {
746 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
748
749 let mut score = create_passing_score();
750 score.components.idiomaticity = 0.4; 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]
768 fn test_high_cache_hit_rate_warning() {
769 let config = QualityGateConfig::default(); let enforcer = QualityGateEnforcer::new(config);
771
772 let mut score = create_passing_score();
773 score.cache_hit_rate = 0.9; 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]
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")?; let config = QualityGateConfig::default(); 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]
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(); let enforcer = QualityGateEnforcer::new(config);
813
814 let mut score = create_passing_score();
815 score.confidence = 0.8; 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]
835 fn test_multiple_violations() {
836 let config = QualityGateConfig::default();
837 let enforcer = QualityGateEnforcer::new(config);
838
839 let score = create_minimal_score(); let result = enforcer.enforce_gates(&score, None);
841
842 assert!(!result.passed);
843 assert!(result.violations.len() >= 3); 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]
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]
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]
914 fn test_violation_enums_coverage() {
915 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 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]
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 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 #[test]
1013 fn test_new_never_panics(input: String) {
1014 let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
1016 let _ = std::panic::catch_unwind(|| {
1018 });
1021 }
1022 }
1023}