1pub mod coverage;
6pub mod ruchy_coverage;
7pub mod instrumentation;
8pub mod scoring;
9pub mod gates;
10pub mod enforcement;
11pub mod formatter;
12pub mod linter;
13
14pub use coverage::{
15 CoverageCollector, CoverageReport, CoverageTool, FileCoverage, HtmlReportGenerator,
16};
17
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct QualityGates {
22 metrics: QualityMetrics,
23 thresholds: QualityThresholds,
24}
25
26#[derive(Default, Debug, Clone, Serialize, Deserialize)]
27pub struct QualityMetrics {
28 pub test_coverage: f64,
29 pub cyclomatic_complexity: u32,
30 pub cognitive_complexity: u32,
31 pub satd_count: usize, pub clippy_warnings: usize,
33 pub documentation_coverage: f64,
34 pub unsafe_blocks: usize,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct QualityThresholds {
39 pub min_test_coverage: f64, pub max_complexity: u32, pub max_satd: usize, pub max_clippy_warnings: usize, pub min_doc_coverage: f64, }
45
46impl Default for QualityThresholds {
47 fn default() -> Self {
48 Self {
49 min_test_coverage: 80.0,
50 max_complexity: 10,
51 max_satd: 0,
52 max_clippy_warnings: 0,
53 min_doc_coverage: 90.0,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub enum Violation {
60 InsufficientCoverage { current: f64, required: f64 },
61 ExcessiveComplexity { current: u32, maximum: u32 },
62 TechnicalDebt { count: usize },
63 ClippyWarnings { count: usize },
64 InsufficientDocumentation { current: f64, required: f64 },
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum QualityReport {
69 Pass,
70 Fail { violations: Vec<Violation> },
71}
72
73impl QualityGates {
74 pub fn new() -> Self {
75 Self {
76 metrics: QualityMetrics::default(),
77 thresholds: QualityThresholds::default(),
78 }
79 }
80
81 pub fn with_thresholds(thresholds: QualityThresholds) -> Self {
82 Self {
83 metrics: QualityMetrics::default(),
84 thresholds,
85 }
86 }
87
88 pub fn update_metrics(&mut self, metrics: QualityMetrics) {
89 self.metrics = metrics;
90 }
91
92 pub fn check(&self) -> Result<QualityReport, QualityReport> {
98 let mut violations = Vec::new();
99
100 if self.metrics.test_coverage < self.thresholds.min_test_coverage {
101 violations.push(Violation::InsufficientCoverage {
102 current: self.metrics.test_coverage,
103 required: self.thresholds.min_test_coverage,
104 });
105 }
106
107 if self.metrics.cyclomatic_complexity > self.thresholds.max_complexity {
108 violations.push(Violation::ExcessiveComplexity {
109 current: self.metrics.cyclomatic_complexity,
110 maximum: self.thresholds.max_complexity,
111 });
112 }
113
114 if self.metrics.satd_count > self.thresholds.max_satd {
115 violations.push(Violation::TechnicalDebt {
116 count: self.metrics.satd_count,
117 });
118 }
119
120 if self.metrics.clippy_warnings > self.thresholds.max_clippy_warnings {
121 violations.push(Violation::ClippyWarnings {
122 count: self.metrics.clippy_warnings,
123 });
124 }
125
126 if self.metrics.documentation_coverage < self.thresholds.min_doc_coverage {
127 violations.push(Violation::InsufficientDocumentation {
128 current: self.metrics.documentation_coverage,
129 required: self.thresholds.min_doc_coverage,
130 });
131 }
132
133 if violations.is_empty() {
134 Ok(QualityReport::Pass)
135 } else {
136 Err(QualityReport::Fail { violations })
137 }
138 }
139
140 pub fn collect_metrics(&mut self) -> Result<QualityMetrics, Box<dyn std::error::Error>> {
146 let satd_count = Self::count_satd_comments()?;
148
149 let mut metrics = QualityMetrics {
150 satd_count,
151 ..Default::default()
152 };
153
154 if let Ok(coverage_report) = Self::collect_coverage() {
156 metrics.test_coverage = coverage_report.line_coverage_percentage();
157 } else {
158 metrics.test_coverage = Self::estimate_coverage()?;
160 }
161
162 metrics.clippy_warnings = 0; self.metrics = metrics.clone();
167 Ok(metrics)
168 }
169
170 fn collect_coverage() -> Result<CoverageReport, Box<dyn std::error::Error>> {
176 let collector = CoverageCollector::new(CoverageTool::Tarpaulin);
178 if collector.is_available() {
179 return collector.collect().map_err(Into::into);
180 }
181
182 let collector = CoverageCollector::new(CoverageTool::Grcov);
184 if collector.is_available() {
185 return collector.collect().map_err(Into::into);
186 }
187
188 let collector = CoverageCollector::new(CoverageTool::Llvm);
190 if collector.is_available() {
191 return collector.collect().map_err(Into::into);
192 }
193
194 Err("No coverage tool available".into())
195 }
196
197 #[allow(clippy::unnecessary_wraps)]
198 #[allow(clippy::unnecessary_wraps)]
204 fn estimate_coverage() -> Result<f64, Box<dyn std::error::Error>> {
205 use std::process::Command;
206
207 let test_files = Command::new("find")
209 .args(["tests", "-name", "*.rs", "-o", "-name", "*test*.rs"])
210 .output()
211 .map(|output| String::from_utf8_lossy(&output.stdout).lines().count())
212 .unwrap_or(0);
213
214 let src_files = Command::new("find")
215 .args(["src", "-name", "*.rs"])
216 .output()
217 .map(|output| String::from_utf8_lossy(&output.stdout).lines().count())
218 .unwrap_or(1);
219
220 #[allow(clippy::cast_precision_loss)]
222 let estimated_coverage = (test_files as f64 / src_files as f64) * 100.0;
223 Ok(estimated_coverage.min(100.0))
224 }
225
226 fn count_satd_comments() -> Result<usize, Box<dyn std::error::Error>> {
227 use std::process::Command;
228
229 let output = Command::new("find")
231 .args([
232 "src",
233 "-name",
234 "*.rs",
235 "-exec",
236 "grep",
237 "-c",
238 "//.*TODO\\|//.*FIXME\\|//.*HACK\\|//.*XXX",
239 "{}",
240 "+",
241 ])
242 .output()?;
243
244 let count = String::from_utf8_lossy(&output.stdout)
245 .lines()
246 .filter_map(|line| line.parse::<usize>().ok())
247 .sum();
248
249 Ok(count)
250 }
251
252 pub fn get_metrics(&self) -> &QualityMetrics {
253 &self.metrics
254 }
255
256 pub fn get_thresholds(&self) -> &QualityThresholds {
257 &self.thresholds
258 }
259
260 pub fn generate_coverage_report(&self) -> Result<(), Box<dyn std::error::Error>> {
266 let coverage_report = Self::collect_coverage()?;
267
268 let html_generator = HtmlReportGenerator::new("target/coverage");
270 html_generator.generate(&coverage_report)?;
271
272 tracing::info!("Coverage Report Summary:");
274 tracing::info!(
275 " Lines: {:.1}% ({}/{})",
276 coverage_report.line_coverage_percentage(),
277 coverage_report.covered_lines,
278 coverage_report.total_lines
279 );
280 tracing::info!(
281 " Functions: {:.1}% ({}/{})",
282 coverage_report.function_coverage_percentage(),
283 coverage_report.covered_functions,
284 coverage_report.total_functions
285 );
286
287 Ok(())
288 }
289}
290
291pub struct CiQualityEnforcer {
293 gates: QualityGates,
294 reporting: ReportingBackend,
295}
296
297pub enum ReportingBackend {
298 Console,
299 Json { output_path: String },
300 GitHub { token: String },
301 Html { output_dir: String },
302}
303
304impl CiQualityEnforcer {
305 pub fn new(gates: QualityGates, reporting: ReportingBackend) -> Self {
306 Self { gates, reporting }
307 }
308
309 #[allow(clippy::cognitive_complexity)]
330 pub fn run_checks(&mut self) -> Result<(), Box<dyn std::error::Error>> {
331 let _metrics = self.gates.collect_metrics()?;
333
334 let report = self.gates.check();
336
337 self.publish_report(&report)?;
339
340 match report {
341 Ok(_) => {
342 tracing::info!("✅ All quality gates passed!");
343
344 if let Err(e) = self.gates.generate_coverage_report() {
346 tracing::warn!("Could not generate coverage report: {e}");
347 }
348
349 Ok(())
350 }
351 Err(QualityReport::Fail { violations }) => {
352 tracing::error!("❌ Quality gate failures:");
353 for violation in violations {
354 tracing::error!(" - {violation:?}");
355 }
356 Err("Quality gate violations detected".into())
357 }
358 Err(QualityReport::Pass) => {
359 Ok(())
361 }
362 }
363 }
364
365 fn publish_report(
366 &self,
367 report: &Result<QualityReport, QualityReport>,
368 ) -> Result<(), Box<dyn std::error::Error>> {
369 match &self.reporting {
370 ReportingBackend::Console => {
371 tracing::info!("Quality Report: {report:?}");
372 }
373 ReportingBackend::Json { output_path } => {
374 let json = serde_json::to_string_pretty(report)?;
375 std::fs::write(output_path, json)?;
376 }
377 ReportingBackend::Html { output_dir } => {
378 if let Ok(coverage_report) = QualityGates::collect_coverage() {
380 let html_generator = HtmlReportGenerator::new(output_dir);
381 html_generator.generate(&coverage_report)?;
382 }
383 }
384 ReportingBackend::GitHub { token: _token } => {
385 tracing::info!("GitHub reporting not yet implemented");
387 }
388 }
389 Ok(())
390 }
391}
392
393impl Default for QualityGates {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_quality_gates_creation() {
405 let gates = QualityGates::new();
406 assert_eq!(gates.thresholds.max_satd, 0);
407 assert!((gates.thresholds.min_test_coverage - 80.0).abs() < f64::EPSILON);
408 }
409
410 #[test]
411 fn test_quality_check_pass() {
412 let mut gates = QualityGates::new();
413
414 gates.update_metrics(QualityMetrics {
416 test_coverage: 95.0,
417 cyclomatic_complexity: 5,
418 cognitive_complexity: 8,
419 satd_count: 0,
420 clippy_warnings: 0,
421 documentation_coverage: 95.0,
422 unsafe_blocks: 0,
423 });
424
425 let result = gates.check();
426 assert!(matches!(result, Ok(QualityReport::Pass)));
427 }
428
429 #[test]
430 fn test_quality_check_fail() {
431 let mut gates = QualityGates::new();
432
433 gates.update_metrics(QualityMetrics {
435 test_coverage: 60.0, cyclomatic_complexity: 15, cognitive_complexity: 20,
438 satd_count: 5, clippy_warnings: 0,
440 documentation_coverage: 70.0, unsafe_blocks: 0,
442 });
443
444 let result = gates.check();
445 if let Err(QualityReport::Fail { violations }) = result {
446 assert_eq!(violations.len(), 4); } else {
448 unreachable!("Expected quality check to fail");
449 }
450 }
451
452 #[test]
453 fn test_satd_count_collection() {
454 let _gates = QualityGates::new();
455 let count = QualityGates::count_satd_comments().unwrap_or(0);
456
457 assert_eq!(count, 0, "SATD comments should be eliminated");
459 }
460
461 #[test]
462 #[ignore = "slow integration test - run with --ignored flag"]
463 fn test_coverage_integration() {
464 let result = QualityGates::collect_coverage();
466 if let Ok(report) = result {
468 assert!(report.line_coverage_percentage() >= 0.0);
469 assert!(report.line_coverage_percentage() <= 100.0);
470 }
471 }
472}