/home/noah/src/ruchy/src/quality/gates.rs
Line | Count | Source |
1 | | //! Quality gate enforcement system (RUCHY-0815) |
2 | | |
3 | | use std::collections::HashMap; |
4 | | use std::path::{Path, PathBuf}; |
5 | | use serde::{Deserialize, Serialize}; |
6 | | use crate::quality::scoring::{QualityScore, Grade}; |
7 | | |
8 | | /// Quality gate configuration |
9 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
10 | | pub struct QualityGateConfig { |
11 | | /// Minimum overall score required (0.0-1.0) |
12 | | pub min_score: f64, |
13 | | |
14 | | /// Minimum grade required |
15 | | pub min_grade: Grade, |
16 | | |
17 | | /// Component-specific thresholds |
18 | | pub component_thresholds: ComponentThresholds, |
19 | | |
20 | | /// Anti-gaming rules |
21 | | pub anti_gaming: AntiGamingRules, |
22 | | |
23 | | /// CI/CD integration settings |
24 | | pub ci_integration: CiIntegration, |
25 | | |
26 | | /// Project-specific overrides |
27 | | pub project_overrides: HashMap<String, f64>, |
28 | | } |
29 | | |
30 | | /// Component-specific quality thresholds |
31 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
32 | | pub struct ComponentThresholds { |
33 | | /// Minimum correctness score (0.0-1.0) |
34 | | pub correctness: f64, |
35 | | |
36 | | /// Minimum performance score (0.0-1.0) |
37 | | pub performance: f64, |
38 | | |
39 | | /// Minimum maintainability score (0.0-1.0) |
40 | | pub maintainability: f64, |
41 | | |
42 | | /// Minimum safety score (0.0-1.0) |
43 | | pub safety: f64, |
44 | | |
45 | | /// Minimum idiomaticity score (0.0-1.0) |
46 | | pub idiomaticity: f64, |
47 | | } |
48 | | |
49 | | /// Anti-gaming rules to prevent score manipulation |
50 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
51 | | pub struct AntiGamingRules { |
52 | | /// Minimum confidence level required (0.0-1.0) |
53 | | pub min_confidence: f64, |
54 | | |
55 | | /// Maximum cache hit rate allowed (0.0-1.0) - prevents stale analysis |
56 | | pub max_cache_hit_rate: f64, |
57 | | |
58 | | /// Require deep analysis for critical files |
59 | | pub require_deep_analysis: Vec<String>, |
60 | | |
61 | | /// Penalty for files that are too small (gaming by splitting) |
62 | | pub min_file_size_bytes: usize, |
63 | | |
64 | | /// Penalty for excessive test file ratios (gaming with trivial tests) |
65 | | pub max_test_ratio: f64, |
66 | | } |
67 | | |
68 | | /// CI/CD integration configuration |
69 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
70 | | pub struct CiIntegration { |
71 | | /// Fail CI/CD pipeline on gate failure |
72 | | pub fail_on_violation: bool, |
73 | | |
74 | | /// Export results in `JUnit` XML format |
75 | | pub junit_xml: bool, |
76 | | |
77 | | /// Export results in JSON format for tooling |
78 | | pub json_output: bool, |
79 | | |
80 | | /// Send notifications on quality degradation |
81 | | pub notifications: NotificationConfig, |
82 | | |
83 | | /// Block merge requests below threshold |
84 | | pub block_merge: bool, |
85 | | } |
86 | | |
87 | | /// Notification configuration |
88 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
89 | | pub struct NotificationConfig { |
90 | | /// Enable Slack notifications |
91 | | pub slack: bool, |
92 | | |
93 | | /// Enable email notifications |
94 | | pub email: bool, |
95 | | |
96 | | /// Webhook URL for custom notifications |
97 | | pub webhook: Option<String>, |
98 | | } |
99 | | |
100 | | /// Quality gate enforcement result |
101 | | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] |
102 | | pub struct GateResult { |
103 | | /// Whether the quality gate passed |
104 | | pub passed: bool, |
105 | | |
106 | | /// Overall score achieved |
107 | | pub score: f64, |
108 | | |
109 | | /// Grade achieved |
110 | | pub grade: Grade, |
111 | | |
112 | | /// Specific violations found |
113 | | pub violations: Vec<Violation>, |
114 | | |
115 | | /// Confidence in the result |
116 | | pub confidence: f64, |
117 | | |
118 | | /// Anti-gaming warnings |
119 | | pub gaming_warnings: Vec<String>, |
120 | | } |
121 | | |
122 | | /// Specific quality gate violation |
123 | | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] |
124 | | pub struct Violation { |
125 | | /// Type of violation |
126 | | pub violation_type: ViolationType, |
127 | | |
128 | | /// Actual value that caused violation |
129 | | pub actual: f64, |
130 | | |
131 | | /// Required threshold |
132 | | pub required: f64, |
133 | | |
134 | | /// Severity of the violation |
135 | | pub severity: Severity, |
136 | | |
137 | | /// Human-readable message |
138 | | pub message: String, |
139 | | } |
140 | | |
141 | | /// Types of quality gate violations |
142 | | #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] |
143 | | pub enum ViolationType { |
144 | | OverallScore, |
145 | | Grade, |
146 | | Correctness, |
147 | | Performance, |
148 | | Maintainability, |
149 | | Safety, |
150 | | Idiomaticity, |
151 | | Confidence, |
152 | | Gaming, |
153 | | } |
154 | | |
155 | | /// Violation severity levels |
156 | | #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] |
157 | | pub enum Severity { |
158 | | Critical, // Must fix to pass |
159 | | High, // Should fix soon |
160 | | Medium, // Should improve |
161 | | Low, // Nice to improve |
162 | | } |
163 | | |
164 | | /// Quality gate enforcer |
165 | | pub struct QualityGateEnforcer { |
166 | | config: QualityGateConfig, |
167 | | } |
168 | | |
169 | | impl Default for QualityGateConfig { |
170 | 0 | fn default() -> Self { |
171 | 0 | Self { |
172 | 0 | min_score: 0.7, // B- grade minimum |
173 | 0 | min_grade: Grade::BMinus, |
174 | 0 | component_thresholds: ComponentThresholds { |
175 | 0 | correctness: 0.8, // High correctness required |
176 | 0 | performance: 0.6, // Moderate performance required |
177 | 0 | maintainability: 0.7, // Good maintainability required |
178 | 0 | safety: 0.8, // High safety required |
179 | 0 | idiomaticity: 0.5, // Basic idiomaticity required |
180 | 0 | }, |
181 | 0 | anti_gaming: AntiGamingRules { |
182 | 0 | min_confidence: 0.6, |
183 | 0 | max_cache_hit_rate: 0.8, |
184 | 0 | require_deep_analysis: vec![ |
185 | 0 | "src/main.rs".to_string(), |
186 | 0 | "src/lib.rs".to_string(), |
187 | 0 | ], |
188 | 0 | min_file_size_bytes: 100, |
189 | 0 | max_test_ratio: 2.0, |
190 | 0 | }, |
191 | 0 | ci_integration: CiIntegration { |
192 | 0 | fail_on_violation: true, |
193 | 0 | junit_xml: true, |
194 | 0 | json_output: true, |
195 | 0 | notifications: NotificationConfig { |
196 | 0 | slack: false, |
197 | 0 | email: false, |
198 | 0 | webhook: None, |
199 | 0 | }, |
200 | 0 | block_merge: true, |
201 | 0 | }, |
202 | 0 | project_overrides: HashMap::new(), |
203 | 0 | } |
204 | 0 | } |
205 | | } |
206 | | |
207 | | impl QualityGateEnforcer { |
208 | 0 | pub fn new(config: QualityGateConfig) -> Self { |
209 | 0 | Self { config } |
210 | 0 | } |
211 | | |
212 | | /// Load configuration from .ruchy/score.toml |
213 | 0 | pub fn load_config(project_root: &Path) -> anyhow::Result<QualityGateConfig> { |
214 | 0 | let config_path = project_root.join(".ruchy").join("score.toml"); |
215 | | |
216 | 0 | if config_path.exists() { |
217 | 0 | let content = std::fs::read_to_string(&config_path)?; |
218 | 0 | let config: QualityGateConfig = toml::from_str(&content)?; |
219 | 0 | Ok(config) |
220 | | } else { |
221 | | // Create default configuration file |
222 | 0 | let default_config = QualityGateConfig::default(); |
223 | 0 | std::fs::create_dir_all(project_root.join(".ruchy"))?; |
224 | 0 | let toml_content = toml::to_string_pretty(&default_config)?; |
225 | 0 | std::fs::write(&config_path, toml_content)?; |
226 | 0 | Ok(default_config) |
227 | | } |
228 | 0 | } |
229 | | |
230 | | /// Enforce quality gates on a score |
231 | 0 | pub fn enforce_gates(&self, score: &QualityScore, file_path: Option<&PathBuf>) -> GateResult { |
232 | 0 | let mut violations = Vec::new(); |
233 | 0 | let mut gaming_warnings = Vec::new(); |
234 | | |
235 | | // Check overall score threshold |
236 | 0 | if score.value < self.config.min_score { |
237 | 0 | violations.push(Violation { |
238 | 0 | violation_type: ViolationType::OverallScore, |
239 | 0 | actual: score.value, |
240 | 0 | required: self.config.min_score, |
241 | 0 | severity: Severity::Critical, |
242 | 0 | message: format!( |
243 | 0 | "Overall score {:.1}% below minimum {:.1}%", |
244 | 0 | score.value * 100.0, |
245 | 0 | self.config.min_score * 100.0 |
246 | 0 | ), |
247 | 0 | }); |
248 | 0 | } |
249 | | |
250 | | // Check grade requirement |
251 | 0 | if score.grade < self.config.min_grade { |
252 | 0 | violations.push(Violation { |
253 | 0 | violation_type: ViolationType::Grade, |
254 | 0 | actual: score.value, |
255 | 0 | required: self.config.min_score, |
256 | 0 | severity: Severity::Critical, |
257 | 0 | message: format!( |
258 | 0 | "Grade {} below minimum {}", |
259 | 0 | score.grade, |
260 | 0 | self.config.min_grade |
261 | 0 | ), |
262 | 0 | }); |
263 | 0 | } |
264 | | |
265 | | // Check component thresholds |
266 | 0 | self.check_component_thresholds(score, &mut violations); |
267 | | |
268 | | // Check anti-gaming rules |
269 | 0 | self.check_anti_gaming_rules(score, file_path, &mut gaming_warnings, &mut violations); |
270 | | |
271 | | // Check confidence threshold |
272 | 0 | if score.confidence < self.config.anti_gaming.min_confidence { |
273 | 0 | violations.push(Violation { |
274 | 0 | violation_type: ViolationType::Confidence, |
275 | 0 | actual: score.confidence, |
276 | 0 | required: self.config.anti_gaming.min_confidence, |
277 | 0 | severity: Severity::High, |
278 | 0 | message: format!( |
279 | 0 | "Confidence {:.1}% below minimum {:.1}%", |
280 | 0 | score.confidence * 100.0, |
281 | 0 | self.config.anti_gaming.min_confidence * 100.0 |
282 | 0 | ), |
283 | 0 | }); |
284 | 0 | } |
285 | | |
286 | 0 | let passed = violations.iter().all(|v| v.severity != Severity::Critical); |
287 | | |
288 | 0 | GateResult { |
289 | 0 | passed, |
290 | 0 | score: score.value, |
291 | 0 | grade: score.grade, |
292 | 0 | violations, |
293 | 0 | confidence: score.confidence, |
294 | 0 | gaming_warnings, |
295 | 0 | } |
296 | 0 | } |
297 | | |
298 | 0 | fn check_component_thresholds(&self, score: &QualityScore, violations: &mut Vec<Violation>) { |
299 | 0 | let thresholds = &self.config.component_thresholds; |
300 | | |
301 | 0 | if score.components.correctness < thresholds.correctness { |
302 | 0 | violations.push(Violation { |
303 | 0 | violation_type: ViolationType::Correctness, |
304 | 0 | actual: score.components.correctness, |
305 | 0 | required: thresholds.correctness, |
306 | 0 | severity: Severity::Critical, |
307 | 0 | message: format!( |
308 | 0 | "Correctness {:.1}% below minimum {:.1}%", |
309 | 0 | score.components.correctness * 100.0, |
310 | 0 | thresholds.correctness * 100.0 |
311 | 0 | ), |
312 | 0 | }); |
313 | 0 | } |
314 | | |
315 | 0 | if score.components.performance < thresholds.performance { |
316 | 0 | violations.push(Violation { |
317 | 0 | violation_type: ViolationType::Performance, |
318 | 0 | actual: score.components.performance, |
319 | 0 | required: thresholds.performance, |
320 | 0 | severity: Severity::High, |
321 | 0 | message: format!( |
322 | 0 | "Performance {:.1}% below minimum {:.1}%", |
323 | 0 | score.components.performance * 100.0, |
324 | 0 | thresholds.performance * 100.0 |
325 | 0 | ), |
326 | 0 | }); |
327 | 0 | } |
328 | | |
329 | 0 | if score.components.maintainability < thresholds.maintainability { |
330 | 0 | violations.push(Violation { |
331 | 0 | violation_type: ViolationType::Maintainability, |
332 | 0 | actual: score.components.maintainability, |
333 | 0 | required: thresholds.maintainability, |
334 | 0 | severity: Severity::High, |
335 | 0 | message: format!( |
336 | 0 | "Maintainability {:.1}% below minimum {:.1}%", |
337 | 0 | score.components.maintainability * 100.0, |
338 | 0 | thresholds.maintainability * 100.0 |
339 | 0 | ), |
340 | 0 | }); |
341 | 0 | } |
342 | | |
343 | 0 | if score.components.safety < thresholds.safety { |
344 | 0 | violations.push(Violation { |
345 | 0 | violation_type: ViolationType::Safety, |
346 | 0 | actual: score.components.safety, |
347 | 0 | required: thresholds.safety, |
348 | 0 | severity: Severity::Critical, |
349 | 0 | message: format!( |
350 | 0 | "Safety {:.1}% below minimum {:.1}%", |
351 | 0 | score.components.safety * 100.0, |
352 | 0 | thresholds.safety * 100.0 |
353 | 0 | ), |
354 | 0 | }); |
355 | 0 | } |
356 | | |
357 | 0 | if score.components.idiomaticity < thresholds.idiomaticity { |
358 | 0 | violations.push(Violation { |
359 | 0 | violation_type: ViolationType::Idiomaticity, |
360 | 0 | actual: score.components.idiomaticity, |
361 | 0 | required: thresholds.idiomaticity, |
362 | 0 | severity: Severity::Medium, |
363 | 0 | message: format!( |
364 | 0 | "Idiomaticity {:.1}% below minimum {:.1}%", |
365 | 0 | score.components.idiomaticity * 100.0, |
366 | 0 | thresholds.idiomaticity * 100.0 |
367 | 0 | ), |
368 | 0 | }); |
369 | 0 | } |
370 | 0 | } |
371 | | |
372 | 0 | fn check_anti_gaming_rules( |
373 | 0 | &self, |
374 | 0 | score: &QualityScore, |
375 | 0 | file_path: Option<&PathBuf>, |
376 | 0 | gaming_warnings: &mut Vec<String>, |
377 | 0 | violations: &mut Vec<Violation>, |
378 | 0 | ) { |
379 | | // Check cache hit rate (prevent stale analysis gaming) |
380 | 0 | if score.cache_hit_rate > self.config.anti_gaming.max_cache_hit_rate { |
381 | 0 | gaming_warnings.push(format!( |
382 | 0 | "High cache hit rate {:.1}% may indicate stale analysis", |
383 | 0 | score.cache_hit_rate * 100.0 |
384 | 0 | )); |
385 | 0 | } |
386 | | |
387 | | // Check file size requirements |
388 | 0 | if let Some(path) = file_path { |
389 | 0 | if let Ok(metadata) = std::fs::metadata(path) { |
390 | 0 | if metadata.len() < self.config.anti_gaming.min_file_size_bytes as u64 { |
391 | 0 | gaming_warnings.push(format!( |
392 | 0 | "File {} is very small ({} bytes) - may indicate gaming by splitting", |
393 | 0 | path.display(), |
394 | 0 | metadata.len() |
395 | 0 | )); |
396 | 0 | } |
397 | 0 | } |
398 | | |
399 | | // Check if critical files require deep analysis |
400 | 0 | let path_str = path.to_string_lossy(); |
401 | 0 | if self.config.anti_gaming.require_deep_analysis.iter().any(|p| path_str.contains(p)) |
402 | 0 | && score.confidence < 0.9 { |
403 | 0 | violations.push(Violation { |
404 | 0 | violation_type: ViolationType::Gaming, |
405 | 0 | actual: score.confidence, |
406 | 0 | required: 0.9, |
407 | 0 | severity: Severity::Critical, |
408 | 0 | message: format!( |
409 | 0 | "Critical file {} requires deep analysis (confidence < 90%)", |
410 | 0 | path.display() |
411 | 0 | ), |
412 | 0 | }); |
413 | 0 | } |
414 | 0 | } |
415 | 0 | } |
416 | | |
417 | | /// Export results for CI/CD integration |
418 | 0 | pub fn export_ci_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> { |
419 | 0 | if self.config.ci_integration.json_output { |
420 | 0 | self.export_json_results(results, output_dir)?; |
421 | 0 | } |
422 | | |
423 | 0 | if self.config.ci_integration.junit_xml { |
424 | 0 | self.export_junit_results(results, output_dir)?; |
425 | 0 | } |
426 | | |
427 | 0 | Ok(()) |
428 | 0 | } |
429 | | |
430 | 0 | fn export_json_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> { |
431 | 0 | let output_path = output_dir.join("quality-gates.json"); |
432 | 0 | let json_content = serde_json::to_string_pretty(results)?; |
433 | 0 | std::fs::write(output_path, json_content)?; |
434 | 0 | Ok(()) |
435 | 0 | } |
436 | | |
437 | 0 | fn export_junit_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> { |
438 | 0 | let output_path = output_dir.join("quality-gates.xml"); |
439 | | |
440 | 0 | let total = results.len(); |
441 | 0 | let failures = results.iter().filter(|r| !r.passed).count(); |
442 | | |
443 | 0 | let mut xml = format!(r#"<?xml version="1.0" encoding="UTF-8"?> |
444 | 0 | <testsuite name="Quality Gates" tests="{total}" failures="{failures}" time="0.0"> |
445 | 0 | "#); |
446 | | |
447 | 0 | for (i, result) in results.iter().enumerate() { |
448 | 0 | let test_name = format!("quality-gate-{i}"); |
449 | 0 | if result.passed { |
450 | 0 | xml.push_str(&format!( |
451 | 0 | r#" <testcase name="{test_name}" classname="QualityGate" time="0.0"/> |
452 | 0 | "# |
453 | 0 | )); |
454 | 0 | } else { |
455 | 0 | xml.push_str(&format!( |
456 | 0 | r#" <testcase name="{}" classname="QualityGate" time="0.0"> |
457 | 0 | <failure message="Quality gate violation">Score: {:.1}%, Grade: {}</failure> |
458 | 0 | </testcase> |
459 | 0 | "#, |
460 | 0 | test_name, |
461 | 0 | result.score * 100.0, |
462 | 0 | result.grade |
463 | 0 | )); |
464 | 0 | } |
465 | | } |
466 | | |
467 | 0 | xml.push_str("</testsuite>\n"); |
468 | 0 | std::fs::write(output_path, xml)?; |
469 | 0 | Ok(()) |
470 | 0 | } |
471 | | } |
472 | | |
473 | | impl PartialOrd for Grade { |
474 | 0 | fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { |
475 | 0 | Some(self.cmp(other)) |
476 | 0 | } |
477 | | } |
478 | | |
479 | | impl Ord for Grade { |
480 | 0 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
481 | | // use std::cmp::Ordering; |
482 | | use Grade::{F, D, CMinus, C, CPlus, BMinus, B, BPlus, AMinus, A, APlus}; |
483 | | |
484 | 0 | let self_rank = match self { |
485 | 0 | F => 0, |
486 | 0 | D => 1, |
487 | 0 | CMinus => 2, |
488 | 0 | C => 3, |
489 | 0 | CPlus => 4, |
490 | 0 | BMinus => 5, |
491 | 0 | B => 6, |
492 | 0 | BPlus => 7, |
493 | 0 | AMinus => 8, |
494 | 0 | A => 9, |
495 | 0 | APlus => 10, |
496 | | }; |
497 | | |
498 | 0 | let other_rank = match other { |
499 | 0 | F => 0, |
500 | 0 | D => 1, |
501 | 0 | CMinus => 2, |
502 | 0 | C => 3, |
503 | 0 | CPlus => 4, |
504 | 0 | BMinus => 5, |
505 | 0 | B => 6, |
506 | 0 | BPlus => 7, |
507 | 0 | AMinus => 8, |
508 | 0 | A => 9, |
509 | 0 | APlus => 10, |
510 | | }; |
511 | | |
512 | 0 | self_rank.cmp(&other_rank) |
513 | 0 | } |
514 | | } |
515 | | |
516 | | // PartialEq and Eq are now derived in scoring.rs |
517 | | |
518 | | #[cfg(test)] |
519 | | mod tests { |
520 | | use super::*; |
521 | | use crate::quality::scoring::{QualityScore, Grade}; |
522 | | use tempfile::TempDir; |
523 | | |
524 | | fn create_minimal_score() -> QualityScore { |
525 | | use crate::quality::scoring::ScoreComponents; |
526 | | QualityScore { |
527 | | value: 0.5, |
528 | | components: ScoreComponents { |
529 | | correctness: 0.5, |
530 | | performance: 0.5, |
531 | | maintainability: 0.5, |
532 | | safety: 0.5, |
533 | | idiomaticity: 0.5, |
534 | | }, |
535 | | grade: Grade::D, |
536 | | confidence: 0.4, |
537 | | cache_hit_rate: 0.3, |
538 | | } |
539 | | } |
540 | | |
541 | | fn create_passing_score() -> QualityScore { |
542 | | use crate::quality::scoring::ScoreComponents; |
543 | | QualityScore { |
544 | | value: 0.85, |
545 | | components: ScoreComponents { |
546 | | correctness: 0.9, |
547 | | performance: 0.8, |
548 | | maintainability: 0.8, |
549 | | safety: 0.9, |
550 | | idiomaticity: 0.7, |
551 | | }, |
552 | | grade: Grade::APlus, |
553 | | confidence: 0.9, |
554 | | cache_hit_rate: 0.2, |
555 | | } |
556 | | } |
557 | | |
558 | | // Test 1: Default Configuration Creation |
559 | | #[test] |
560 | | fn test_default_quality_gate_config() { |
561 | | let config = QualityGateConfig::default(); |
562 | | |
563 | | assert_eq!(config.min_score, 0.7); |
564 | | assert_eq!(config.min_grade, Grade::BMinus); |
565 | | assert_eq!(config.component_thresholds.correctness, 0.8); |
566 | | assert_eq!(config.component_thresholds.safety, 0.8); |
567 | | assert_eq!(config.anti_gaming.min_confidence, 0.6); |
568 | | assert!(config.ci_integration.fail_on_violation); |
569 | | assert!(config.project_overrides.is_empty()); |
570 | | } |
571 | | |
572 | | // Test 2: Quality Gate Enforcer Creation |
573 | | #[test] |
574 | | fn test_quality_gate_enforcer_creation() { |
575 | | let config = QualityGateConfig::default(); |
576 | | let enforcer = QualityGateEnforcer::new(config); |
577 | | |
578 | | // Verify enforcer uses the provided config |
579 | | let score = create_minimal_score(); |
580 | | let result = enforcer.enforce_gates(&score, None); |
581 | | |
582 | | // Should fail with default thresholds |
583 | | assert!(!result.passed); |
584 | | assert!(!result.violations.is_empty()); |
585 | | } |
586 | | |
587 | | // Test 3: Passing Quality Gate - All Criteria Met |
588 | | #[test] |
589 | | fn test_quality_gate_passes_with_high_score() { |
590 | | let config = QualityGateConfig::default(); |
591 | | let enforcer = QualityGateEnforcer::new(config); |
592 | | let score = create_passing_score(); |
593 | | |
594 | | let result = enforcer.enforce_gates(&score, None); |
595 | | |
596 | | assert!(result.passed, "High quality score should pass all gates"); |
597 | | assert_eq!(result.score, 0.85); |
598 | | assert_eq!(result.grade, Grade::APlus); |
599 | | assert!(result.violations.is_empty()); |
600 | | assert_eq!(result.confidence, 0.9); |
601 | | assert!(result.gaming_warnings.is_empty()); |
602 | | } |
603 | | |
604 | | // Test 4: Failing Overall Score Threshold |
605 | | #[test] |
606 | | fn test_quality_gate_fails_overall_score() { |
607 | | let config = QualityGateConfig::default(); // min_score: 0.7 |
608 | | let enforcer = QualityGateEnforcer::new(config); |
609 | | |
610 | | let mut score = create_minimal_score(); |
611 | | score.value = 0.6; // Below 0.7 threshold |
612 | | |
613 | | let result = enforcer.enforce_gates(&score, None); |
614 | | |
615 | | assert!(!result.passed, "Score below threshold should fail"); |
616 | | |
617 | | // Should have overall score violation |
618 | | let overall_violations: Vec<_> = result.violations.iter() |
619 | | .filter(|v| v.violation_type == ViolationType::OverallScore) |
620 | | .collect(); |
621 | | assert_eq!(overall_violations.len(), 1); |
622 | | |
623 | | let violation = &overall_violations[0]; |
624 | | assert_eq!(violation.actual, 0.6); |
625 | | assert_eq!(violation.required, 0.7); |
626 | | assert_eq!(violation.severity, Severity::Critical); |
627 | | assert!(violation.message.contains("60.0%")); |
628 | | assert!(violation.message.contains("70.0%")); |
629 | | } |
630 | | |
631 | | // Test 5: Confidence Threshold Violation |
632 | | #[test] |
633 | | fn test_confidence_threshold_violation() { |
634 | | let config = QualityGateConfig::default(); // min_confidence: 0.6 |
635 | | let enforcer = QualityGateEnforcer::new(config); |
636 | | |
637 | | let mut score = create_passing_score(); |
638 | | score.confidence = 0.4; // Below 0.6 threshold |
639 | | |
640 | | let result = enforcer.enforce_gates(&score, None); |
641 | | |
642 | | let confidence_violations: Vec<_> = result.violations.iter() |
643 | | .filter(|v| v.violation_type == ViolationType::Confidence) |
644 | | .collect(); |
645 | | assert_eq!(confidence_violations.len(), 1); |
646 | | |
647 | | let violation = &confidence_violations[0]; |
648 | | assert_eq!(violation.severity, Severity::High); |
649 | | assert_eq!(violation.actual, 0.4); |
650 | | assert_eq!(violation.required, 0.6); |
651 | | } |
652 | | |
653 | | // Test 6: Configuration File Loading (Success) |
654 | | #[test] |
655 | | fn test_load_config_creates_default() { |
656 | | let temp_dir = TempDir::new().unwrap(); |
657 | | let project_root = temp_dir.path(); |
658 | | |
659 | | let config = QualityGateEnforcer::load_config(project_root).unwrap(); |
660 | | |
661 | | // Should create default config |
662 | | assert_eq!(config.min_score, 0.7); |
663 | | assert_eq!(config.min_grade, Grade::BMinus); |
664 | | |
665 | | // Should create .ruchy/score.toml file |
666 | | let config_path = project_root.join(".ruchy").join("score.toml"); |
667 | | assert!(config_path.exists(), "Config file should be created"); |
668 | | |
669 | | // File should contain valid TOML |
670 | | let content = std::fs::read_to_string(config_path).unwrap(); |
671 | | assert!(content.contains("min_score")); |
672 | | assert!(content.contains("0.7")); |
673 | | } |
674 | | |
675 | | // Test 7: Serialization/Deserialization |
676 | | #[test] |
677 | | fn test_config_serialization() { |
678 | | let original_config = QualityGateConfig::default(); |
679 | | |
680 | | // Serialize to TOML |
681 | | let toml_content = toml::to_string(&original_config).unwrap(); |
682 | | assert!(toml_content.contains("min_score")); |
683 | | |
684 | | // Deserialize back |
685 | | let deserialized_config: QualityGateConfig = toml::from_str(&toml_content).unwrap(); |
686 | | assert_eq!(deserialized_config.min_score, original_config.min_score); |
687 | | assert_eq!(deserialized_config.min_grade, original_config.min_grade); |
688 | | } |
689 | | } |