1use crate::error::{Result, SklearsError};
84use serde::{Deserialize, Serialize};
85use std::collections::HashMap;
86use std::fs;
87use std::path::PathBuf;
88use std::process::Command;
89use std::time::{SystemTime, UNIX_EPOCH};
90
91#[derive(Debug)]
93pub struct CoverageCollector {
94 config: CoverageConfig,
95 collected_data: Option<RawCoverageData>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct CoverageConfig {
101 pub minimum_coverage: f64,
103 pub module_thresholds: HashMap<String, f64>,
105 pub output_formats: Vec<String>,
107 pub exclude_patterns: Vec<String>,
109 pub include_patterns: Vec<String>,
111 pub output_directory: PathBuf,
113 pub coverage_tool: CoverageTool,
115 pub fail_on_regression: bool,
117 pub baseline_coverage: Option<f64>,
119}
120
121impl Default for CoverageConfig {
122 fn default() -> Self {
123 Self {
124 minimum_coverage: 80.0,
125 module_thresholds: HashMap::new(),
126 output_formats: vec!["html".to_string(), "json".to_string()],
127 exclude_patterns: vec![
128 "tests/*".to_string(),
129 "benches/*".to_string(),
130 "examples/*".to_string(),
131 ],
132 include_patterns: Vec::new(),
133 output_directory: PathBuf::from("target/coverage"),
134 coverage_tool: CoverageTool::LlvmCov,
135 fail_on_regression: true,
136 baseline_coverage: None,
137 }
138 }
139}
140
141impl CoverageConfig {
142 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn with_minimum_coverage(mut self, threshold: f64) -> Self {
149 self.minimum_coverage = threshold;
150 self
151 }
152
153 pub fn with_output_format(mut self, formats: Vec<&str>) -> Self {
155 self.output_formats = formats.into_iter().map(String::from).collect();
156 self
157 }
158
159 pub fn with_exclude_patterns(mut self, patterns: Vec<&str>) -> Self {
161 self.exclude_patterns = patterns.into_iter().map(String::from).collect();
162 self
163 }
164
165 pub fn with_module_threshold(mut self, module: &str, threshold: f64) -> Self {
167 self.module_thresholds.insert(module.to_string(), threshold);
168 self
169 }
170
171 pub fn with_baseline_coverage(mut self, baseline: f64) -> Self {
173 self.baseline_coverage = Some(baseline);
174 self
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum CoverageTool {
181 LlvmCov,
183 Tarpaulin,
185 Manual,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct RawCoverageData {
192 pub tool: String,
193 pub timestamp: u64,
194 pub files: Vec<FileCoverage>,
195 pub summary: CoverageSummary,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct FileCoverage {
201 pub path: String,
202 pub functions: Vec<FunctionCoverage>,
203 pub lines: Vec<LineCoverage>,
204 pub branches: Vec<BranchCoverage>,
205 pub summary: CoverageSummary,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct FunctionCoverage {
211 pub name: String,
212 pub line_start: u32,
213 pub line_end: u32,
214 pub execution_count: u64,
215 pub covered: bool,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct LineCoverage {
221 pub line_number: u32,
222 pub execution_count: u64,
223 pub covered: bool,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct BranchCoverage {
229 pub line_number: u32,
230 pub branch_id: u32,
231 pub taken_count: u64,
232 pub total_count: u64,
233 pub covered: bool,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct CoverageSummary {
239 pub lines_covered: u32,
240 pub lines_total: u32,
241 pub functions_covered: u32,
242 pub functions_total: u32,
243 pub branches_covered: u32,
244 pub branches_total: u32,
245}
246
247impl CoverageSummary {
248 pub fn line_coverage(&self) -> f64 {
250 if self.lines_total == 0 {
251 100.0
252 } else {
253 (self.lines_covered as f64 / self.lines_total as f64) * 100.0
254 }
255 }
256
257 pub fn function_coverage(&self) -> f64 {
259 if self.functions_total == 0 {
260 100.0
261 } else {
262 (self.functions_covered as f64 / self.functions_total as f64) * 100.0
263 }
264 }
265
266 pub fn branch_coverage(&self) -> f64 {
268 if self.branches_total == 0 {
269 100.0
270 } else {
271 (self.branches_covered as f64 / self.branches_total as f64) * 100.0
272 }
273 }
274}
275
276#[derive(Debug, Serialize, Deserialize)]
278pub struct CoverageReport {
279 pub timestamp: u64,
280 pub config: CoverageConfig,
281 pub overall_summary: CoverageSummary,
282 pub module_summaries: HashMap<String, CoverageSummary>,
283 pub quality_gates: QualityGatesResult,
284 pub recommendations: Vec<CoverageRecommendation>,
285 pub trends: Option<CoverageTrends>,
286}
287
288impl CoverageReport {
289 pub fn overall_coverage(&self) -> f64 {
291 self.overall_summary.line_coverage()
292 }
293
294 pub fn meets_quality_gates(&self) -> bool {
296 self.quality_gates.passed
297 }
298
299 pub fn summary(&self) -> String {
301 format!(
302 "Coverage Report Summary:\n\
303 - Overall coverage: {:.1}%\n\
304 - Lines covered: {}/{}\n\
305 - Functions covered: {}/{}\n\
306 - Branches covered: {}/{}\n\
307 - Quality gates: {}\n\
308 - Recommendations: {}",
309 self.overall_coverage(),
310 self.overall_summary.lines_covered,
311 self.overall_summary.lines_total,
312 self.overall_summary.functions_covered,
313 self.overall_summary.functions_total,
314 self.overall_summary.branches_covered,
315 self.overall_summary.branches_total,
316 if self.quality_gates.passed {
317 "PASSED"
318 } else {
319 "FAILED"
320 },
321 self.recommendations.len()
322 )
323 }
324}
325
326#[derive(Debug, Serialize, Deserialize)]
328pub struct QualityGatesResult {
329 pub passed: bool,
330 pub failures: Vec<QualityGateFailure>,
331 pub warnings: Vec<QualityGateWarning>,
332}
333
334#[derive(Debug, Serialize, Deserialize)]
336pub struct QualityGateFailure {
337 pub rule: String,
338 pub expected: f64,
339 pub actual: f64,
340 pub module: Option<String>,
341}
342
343#[derive(Debug, Serialize, Deserialize)]
345pub struct QualityGateWarning {
346 pub message: String,
347 pub severity: WarningSeverity,
348}
349
350#[derive(Debug, Serialize, Deserialize)]
352pub enum WarningSeverity {
353 Info,
354 Warning,
355 Error,
356}
357
358#[derive(Debug, Serialize, Deserialize)]
360pub struct CoverageRecommendation {
361 pub priority: RecommendationPriority,
362 pub category: RecommendationCategory,
363 pub description: String,
364 pub affected_files: Vec<String>,
365 pub estimated_impact: f64, }
367
368#[derive(Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)]
370pub enum RecommendationPriority {
371 Critical,
372 High,
373 Medium,
374 Low,
375}
376
377#[derive(Debug, Serialize, Deserialize)]
379pub enum RecommendationCategory {
380 UncoveredCriticalCode,
381 MissingBranchTests,
382 UncoveredErrorPaths,
383 LowFunctionCoverage,
384 TestGaps,
385}
386
387#[derive(Debug, Serialize, Deserialize)]
389pub struct CoverageTrends {
390 pub historical_data: Vec<HistoricalCoveragePoint>,
391 pub trend_direction: TrendDirection,
392 pub trend_strength: f64, }
394
395#[derive(Debug, Serialize, Deserialize)]
397pub struct HistoricalCoveragePoint {
398 pub timestamp: u64,
399 pub coverage: f64,
400 pub commit_hash: Option<String>,
401}
402
403#[derive(Debug, Serialize, Deserialize)]
405pub enum TrendDirection {
406 Improving,
407 Stable,
408 Declining,
409}
410
411impl CoverageCollector {
412 pub fn new(config: CoverageConfig) -> Self {
414 Self {
415 config,
416 collected_data: None,
417 }
418 }
419
420 pub fn collect_and_analyze(&mut self) -> Result<CoverageReport> {
422 self.collect_coverage_data()?;
424
425 let report = self.analyze_coverage()?;
427
428 self.generate_outputs(&report)?;
430
431 Ok(report)
432 }
433
434 fn collect_coverage_data(&mut self) -> Result<()> {
436 let raw_data = match self.config.coverage_tool {
437 CoverageTool::LlvmCov => self.collect_llvm_cov_data()?,
438 CoverageTool::Tarpaulin => self.collect_tarpaulin_data()?,
439 CoverageTool::Manual => self.collect_manual_data()?,
440 };
441
442 self.collected_data = Some(raw_data);
443 Ok(())
444 }
445
446 fn collect_llvm_cov_data(&self) -> Result<RawCoverageData> {
448 let output = Command::new("cargo")
450 .args([
451 "llvm-cov",
452 "--json",
453 "--output-path",
454 &format!("{}/llvm-cov.json", self.config.output_directory.display()),
455 ])
456 .output()
457 .map_err(|e| SklearsError::InvalidOperation(format!("Failed to run llvm-cov: {e}")))?;
458
459 if !output.status.success() {
460 return Err(SklearsError::InvalidOperation(format!(
461 "llvm-cov failed: {}",
462 String::from_utf8_lossy(&output.stderr)
463 )));
464 }
465
466 Ok(self.simulate_coverage_data("llvm-cov"))
468 }
469
470 fn collect_tarpaulin_data(&self) -> Result<RawCoverageData> {
472 let output = Command::new("cargo")
473 .args([
474 "tarpaulin",
475 "--out",
476 "Json",
477 "--output-dir",
478 &self.config.output_directory.to_string_lossy(),
479 ])
480 .output()
481 .map_err(|e| SklearsError::InvalidOperation(format!("Failed to run tarpaulin: {e}")))?;
482
483 if !output.status.success() {
484 return Err(SklearsError::InvalidOperation(format!(
485 "tarpaulin failed: {}",
486 String::from_utf8_lossy(&output.stderr)
487 )));
488 }
489
490 Ok(self.simulate_coverage_data("tarpaulin"))
492 }
493
494 fn collect_manual_data(&self) -> Result<RawCoverageData> {
496 Ok(self.simulate_coverage_data("manual"))
498 }
499
500 fn simulate_coverage_data(&self, tool: &str) -> RawCoverageData {
502 let timestamp = SystemTime::now()
503 .duration_since(UNIX_EPOCH)
504 .unwrap()
505 .as_secs();
506
507 RawCoverageData {
508 tool: tool.to_string(),
509 timestamp,
510 files: vec![FileCoverage {
511 path: "src/lib.rs".to_string(),
512 functions: vec![FunctionCoverage {
513 name: "example_function".to_string(),
514 line_start: 10,
515 line_end: 20,
516 execution_count: 5,
517 covered: true,
518 }],
519 lines: vec![
520 LineCoverage {
521 line_number: 15,
522 execution_count: 5,
523 covered: true,
524 },
525 LineCoverage {
526 line_number: 16,
527 execution_count: 0,
528 covered: false,
529 },
530 ],
531 branches: vec![BranchCoverage {
532 line_number: 17,
533 branch_id: 1,
534 taken_count: 3,
535 total_count: 5,
536 covered: true,
537 }],
538 summary: CoverageSummary {
539 lines_covered: 45,
540 lines_total: 50,
541 functions_covered: 8,
542 functions_total: 10,
543 branches_covered: 12,
544 branches_total: 15,
545 },
546 }],
547 summary: CoverageSummary {
548 lines_covered: 850,
549 lines_total: 1000,
550 functions_covered: 75,
551 functions_total: 90,
552 branches_covered: 120,
553 branches_total: 150,
554 },
555 }
556 }
557
558 fn analyze_coverage(&self) -> Result<CoverageReport> {
560 let data = self.collected_data.as_ref().ok_or_else(|| {
561 SklearsError::InvalidOperation("No coverage data collected".to_string())
562 })?;
563
564 let timestamp = SystemTime::now()
565 .duration_since(UNIX_EPOCH)
566 .unwrap()
567 .as_secs();
568
569 let quality_gates = self.evaluate_quality_gates(&data.summary);
571
572 let recommendations = self.generate_recommendations(data);
574
575 let mut module_summaries = HashMap::new();
577 for file in &data.files {
578 let module_name = self.extract_module_name(&file.path);
579 module_summaries.insert(module_name, file.summary.clone());
580 }
581
582 Ok(CoverageReport {
583 timestamp,
584 config: self.config.clone(),
585 overall_summary: data.summary.clone(),
586 module_summaries,
587 quality_gates,
588 recommendations,
589 trends: None, })
591 }
592
593 fn evaluate_quality_gates(&self, summary: &CoverageSummary) -> QualityGatesResult {
595 let mut failures = Vec::new();
596 let mut warnings = Vec::new();
597
598 let overall_coverage = summary.line_coverage();
600 if overall_coverage < self.config.minimum_coverage {
601 failures.push(QualityGateFailure {
602 rule: "Minimum overall coverage".to_string(),
603 expected: self.config.minimum_coverage,
604 actual: overall_coverage,
605 module: None,
606 });
607 }
608
609 if let Some(baseline) = self.config.baseline_coverage {
611 if overall_coverage < baseline - 1.0 {
612 failures.push(QualityGateFailure {
614 rule: "Coverage regression".to_string(),
615 expected: baseline,
616 actual: overall_coverage,
617 module: None,
618 });
619 }
620 }
621
622 let function_coverage = summary.function_coverage();
624 if function_coverage < 80.0 {
625 warnings.push(QualityGateWarning {
626 message: format!("Low function coverage: {function_coverage:.1}%"),
627 severity: WarningSeverity::Warning,
628 });
629 }
630
631 QualityGatesResult {
632 passed: failures.is_empty(),
633 failures,
634 warnings,
635 }
636 }
637
638 fn generate_recommendations(&self, data: &RawCoverageData) -> Vec<CoverageRecommendation> {
640 let mut recommendations = Vec::new();
641
642 for file in &data.files {
644 let uncovered_lines: Vec<_> = file.lines.iter().filter(|line| !line.covered).collect();
645
646 if !uncovered_lines.is_empty() {
647 recommendations.push(CoverageRecommendation {
648 priority: RecommendationPriority::Medium,
649 category: RecommendationCategory::TestGaps,
650 description: format!(
651 "Add tests for {} uncovered lines in {}",
652 uncovered_lines.len(),
653 file.path
654 ),
655 affected_files: vec![file.path.clone()],
656 estimated_impact: (uncovered_lines.len() as f64 / file.lines.len() as f64)
657 * 100.0,
658 });
659 }
660 }
661
662 recommendations.sort_by(|a, b| {
664 a.priority
665 .cmp(&b.priority)
666 .then_with(|| b.estimated_impact.partial_cmp(&a.estimated_impact).unwrap())
667 });
668
669 recommendations
670 }
671
672 fn extract_module_name(&self, path: &str) -> String {
674 if let Some(pos) = path.rfind('/') {
675 if let Some(dot_pos) = path[pos..].find('.') {
676 return path[pos + 1..pos + dot_pos].to_string();
677 }
678 }
679 path.to_string()
680 }
681
682 fn generate_outputs(&self, report: &CoverageReport) -> Result<()> {
684 fs::create_dir_all(&self.config.output_directory).map_err(|e| {
685 SklearsError::InvalidOperation(format!("Failed to create output directory: {e}"))
686 })?;
687
688 for format in &self.config.output_formats {
689 match format.as_str() {
690 "json" => self.generate_json_output(report)?,
691 "html" => self.generate_html_output(report)?,
692 "xml" => self.generate_xml_output(report)?,
693 "text" => self.generate_text_output(report)?,
694 _ => {
695 eprintln!("Warning: Unknown output format '{format}'");
696 }
697 }
698 }
699
700 Ok(())
701 }
702
703 fn generate_json_output(&self, report: &CoverageReport) -> Result<()> {
705 let json = serde_json::to_string_pretty(report).map_err(|e| {
706 SklearsError::InvalidOperation(format!("Failed to serialize JSON: {e}"))
707 })?;
708
709 let path = self.config.output_directory.join("coverage.json");
710 fs::write(path, json).map_err(|e| {
711 SklearsError::InvalidOperation(format!("Failed to write JSON output: {e}"))
712 })
713 }
714
715 fn generate_html_output(&self, report: &CoverageReport) -> Result<()> {
717 let html = self.generate_html_content(report);
718 let path = self.config.output_directory.join("coverage.html");
719 fs::write(path, html).map_err(|e| {
720 SklearsError::InvalidOperation(format!("Failed to write HTML output: {e}"))
721 })
722 }
723
724 fn generate_xml_output(&self, report: &CoverageReport) -> Result<()> {
726 let xml = self.generate_xml_content(report);
727 let path = self.config.output_directory.join("coverage.xml");
728 fs::write(path, xml)
729 .map_err(|e| SklearsError::InvalidOperation(format!("Failed to write XML output: {e}")))
730 }
731
732 fn generate_text_output(&self, report: &CoverageReport) -> Result<()> {
734 let text = report.summary();
735 let path = self.config.output_directory.join("coverage.txt");
736 fs::write(path, text).map_err(|e| {
737 SklearsError::InvalidOperation(format!("Failed to write text output: {e}"))
738 })
739 }
740
741 fn generate_html_content(&self, report: &CoverageReport) -> String {
743 format!(
744 r#"<!DOCTYPE html>
745<html>
746<head>
747 <title>Code Coverage Report</title>
748 <style>
749 body {{ font-family: Arial, sans-serif; margin: 40px; }}
750 .summary {{ background: #f5f5f5; padding: 20px; border-radius: 5px; }}
751 .metric {{ margin: 10px 0; }}
752 .pass {{ color: green; }}
753 .fail {{ color: red; }}
754 .warning {{ color: orange; }}
755 table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
756 th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
757 th {{ background-color: #f2f2f2; }}
758 .coverage-bar {{
759 width: 100px;
760 height: 20px;
761 background: #ddd;
762 border-radius: 10px;
763 overflow: hidden;
764 }}
765 .coverage-fill {{
766 height: 100%;
767 background: linear-gradient(to right, #ff4444, #ffaa00, #44ff44);
768 }}
769 </style>
770</head>
771<body>
772 <h1>Code Coverage Report</h1>
773
774 <div class="summary">
775 <h2>Summary</h2>
776 <div class="metric">Overall Coverage: <strong>{:.1}%</strong></div>
777 <div class="metric">Lines: {}/{} ({:.1}%)</div>
778 <div class="metric">Functions: {}/{} ({:.1}%)</div>
779 <div class="metric">Branches: {}/{} ({:.1}%)</div>
780 <div class="metric">Quality Gates: <span class="{}">{}</span></div>
781 </div>
782
783 <h2>Quality Gates</h2>
784 {}
785
786 <h2>Recommendations</h2>
787 {}
788
789 <p><em>Generated at: {}</em></p>
790</body>
791</html>"#,
792 report.overall_coverage(),
793 report.overall_summary.lines_covered,
794 report.overall_summary.lines_total,
795 report.overall_summary.line_coverage(),
796 report.overall_summary.functions_covered,
797 report.overall_summary.functions_total,
798 report.overall_summary.function_coverage(),
799 report.overall_summary.branches_covered,
800 report.overall_summary.branches_total,
801 report.overall_summary.branch_coverage(),
802 if report.quality_gates.passed {
803 "pass"
804 } else {
805 "fail"
806 },
807 if report.quality_gates.passed {
808 "PASSED"
809 } else {
810 "FAILED"
811 },
812 if report.quality_gates.failures.is_empty() {
813 "<p class=\"pass\">All quality gates passed!</p>".to_string()
814 } else {
815 format!(
816 "<ul>{}</ul>",
817 report
818 .quality_gates
819 .failures
820 .iter()
821 .map(|f| format!(
822 "<li class=\"fail\">{}: Expected {:.1}%, got {:.1}%</li>",
823 f.rule, f.expected, f.actual
824 ))
825 .collect::<Vec<_>>()
826 .join("")
827 )
828 },
829 if report.recommendations.is_empty() {
830 "<p>No recommendations at this time.</p>".to_string()
831 } else {
832 format!(
833 "<ul>{}</ul>",
834 report
835 .recommendations
836 .iter()
837 .map(|r| format!("<li>{}</li>", r.description))
838 .collect::<Vec<_>>()
839 .join("")
840 )
841 },
842 chrono::DateTime::from_timestamp(report.timestamp as i64, 0)
843 .unwrap_or_default()
844 .format("%Y-%m-%d %H:%M:%S UTC")
845 )
846 }
847
848 fn generate_xml_content(&self, report: &CoverageReport) -> String {
850 format!(
851 r#"<?xml version="1.0" encoding="UTF-8"?>
852<coverage timestamp="{}" lines-covered="{}" lines-valid="{}" line-rate="{:.4}">
853 <packages>
854 <package name="sklears" line-rate="{:.4}" branch-rate="{:.4}">
855 <classes>
856 {}
857 </classes>
858 </package>
859 </packages>
860</coverage>"#,
861 report.timestamp,
862 report.overall_summary.lines_covered,
863 report.overall_summary.lines_total,
864 report.overall_summary.line_coverage() / 100.0,
865 report.overall_summary.line_coverage() / 100.0,
866 report.overall_summary.branch_coverage() / 100.0,
867 report
868 .module_summaries
869 .iter()
870 .map(|(name, summary)| {
871 format!(
872 r#"<class name="{}" line-rate="{:.4}" branch-rate="{:.4}"></class>"#,
873 name,
874 summary.line_coverage() / 100.0,
875 summary.branch_coverage() / 100.0
876 )
877 })
878 .collect::<Vec<_>>()
879 .join("\n ")
880 )
881 }
882}
883
884#[derive(Debug)]
886pub struct CoverageCI {
887 config: CIDConfig,
888}
889
890#[derive(Debug, Clone)]
892pub struct CIDConfig {
893 pub pr_coverage_threshold: f64,
894 pub diff_coverage_threshold: f64,
895 pub failure_on_regression: bool,
896 pub post_results_to_pr: bool,
897 pub badge_generation: bool,
898}
899
900impl Default for CIDConfig {
901 fn default() -> Self {
902 Self {
903 pr_coverage_threshold: 80.0,
904 diff_coverage_threshold: 90.0,
905 failure_on_regression: true,
906 post_results_to_pr: false,
907 badge_generation: true,
908 }
909 }
910}
911
912impl CIDConfig {
913 pub fn new() -> Self {
914 Self::default()
915 }
916
917 pub fn with_pr_coverage_threshold(mut self, threshold: f64) -> Self {
918 self.pr_coverage_threshold = threshold;
919 self
920 }
921
922 pub fn with_diff_coverage_threshold(mut self, threshold: f64) -> Self {
923 self.diff_coverage_threshold = threshold;
924 self
925 }
926
927 pub fn with_failure_on_regression(mut self, enabled: bool) -> Self {
928 self.failure_on_regression = enabled;
929 self
930 }
931}
932
933#[derive(Debug)]
935pub struct CICoverageResult {
936 pub coverage: f64,
937 pub diff_coverage: Option<f64>,
938 pub passed: bool,
939 pub failures: Vec<String>,
940}
941
942impl CoverageCI {
943 pub fn new(config: CIDConfig) -> Self {
944 Self { config }
945 }
946
947 pub fn run_coverage_check(&self) -> std::result::Result<CICoverageResult, Vec<String>> {
949 let mut failures = Vec::new();
950
951 let coverage_config =
953 CoverageConfig::new().with_minimum_coverage(self.config.pr_coverage_threshold);
954
955 let mut collector = CoverageCollector::new(coverage_config);
956 let report = match collector.collect_and_analyze() {
957 Ok(report) => report,
958 Err(e) => {
959 failures.push(format!("Failed to collect coverage: {e}"));
960 return Err(failures);
961 }
962 };
963
964 let coverage = report.overall_coverage();
965
966 if coverage < self.config.pr_coverage_threshold {
968 failures.push(format!(
969 "Coverage {:.1}% below PR threshold {:.1}%",
970 coverage, self.config.pr_coverage_threshold
971 ));
972 }
973
974 if !report.meets_quality_gates() {
976 for failure in &report.quality_gates.failures {
977 failures.push(format!(
978 "{}: {:.1}% < {:.1}%",
979 failure.rule, failure.actual, failure.expected
980 ));
981 }
982 }
983
984 let result = CICoverageResult {
985 coverage,
986 diff_coverage: None, passed: failures.is_empty(),
988 failures: failures.clone(),
989 };
990
991 if failures.is_empty() {
992 Ok(result)
993 } else {
994 Err(failures)
995 }
996 }
997}
998
999#[allow(non_snake_case)]
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 #[test]
1005 fn test_coverage_config_creation() {
1006 let config = CoverageConfig::new()
1007 .with_minimum_coverage(85.0)
1008 .with_output_format(vec!["json", "html"])
1009 .with_exclude_patterns(vec!["tests/*"]);
1010
1011 assert_eq!(config.minimum_coverage, 85.0);
1012 assert_eq!(config.output_formats, vec!["json", "html"]);
1013 assert_eq!(config.exclude_patterns, vec!["tests/*"]);
1014 }
1015
1016 #[test]
1017 fn test_coverage_summary_calculations() {
1018 let summary = CoverageSummary {
1019 lines_covered: 80,
1020 lines_total: 100,
1021 functions_covered: 9,
1022 functions_total: 10,
1023 branches_covered: 15,
1024 branches_total: 20,
1025 };
1026
1027 assert_eq!(summary.line_coverage(), 80.0);
1028 assert_eq!(summary.function_coverage(), 90.0);
1029 assert_eq!(summary.branch_coverage(), 75.0);
1030 }
1031
1032 #[test]
1033 fn test_quality_gates_evaluation() {
1034 let config = CoverageConfig::new().with_minimum_coverage(85.0);
1035 let collector = CoverageCollector::new(config);
1036
1037 let summary = CoverageSummary {
1038 lines_covered: 80,
1039 lines_total: 100,
1040 functions_covered: 8,
1041 functions_total: 10,
1042 branches_covered: 12,
1043 branches_total: 15,
1044 };
1045
1046 let result = collector.evaluate_quality_gates(&summary);
1047 assert!(!result.passed);
1048 assert_eq!(result.failures.len(), 1);
1049 assert_eq!(result.failures[0].rule, "Minimum overall coverage");
1050 }
1051
1052 #[test]
1053 fn test_coverage_collector_creation() {
1054 let config = CoverageConfig::new();
1055 let collector = CoverageCollector::new(config);
1056 assert!(collector.collected_data.is_none());
1057 }
1058
1059 #[test]
1060 fn test_ci_config_creation() {
1061 let config = CIDConfig::new()
1062 .with_pr_coverage_threshold(85.0)
1063 .with_diff_coverage_threshold(95.0)
1064 .with_failure_on_regression(false);
1065
1066 assert_eq!(config.pr_coverage_threshold, 85.0);
1067 assert_eq!(config.diff_coverage_threshold, 95.0);
1068 assert!(!config.failure_on_regression);
1069 }
1070
1071 #[test]
1072 fn test_coverage_ci_creation() {
1073 let config = CIDConfig::new();
1074 let ci = CoverageCI::new(config);
1075 assert_eq!(ci.config.pr_coverage_threshold, 80.0);
1076 }
1077
1078 #[test]
1079 fn test_recommendation_priority_ordering() {
1080 let critical = RecommendationPriority::Critical;
1081 let high = RecommendationPriority::High;
1082 let medium = RecommendationPriority::Medium;
1083 let low = RecommendationPriority::Low;
1084
1085 assert!(critical < high);
1086 assert!(high < medium);
1087 assert!(medium < low);
1088 }
1089}