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 .expect("expected valid value")
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 .expect("expected valid value")
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.cmp(&b.priority).then_with(|| {
665 b.estimated_impact
666 .partial_cmp(&a.estimated_impact)
667 .unwrap_or(std::cmp::Ordering::Equal)
668 })
669 });
670
671 recommendations
672 }
673
674 fn extract_module_name(&self, path: &str) -> String {
676 if let Some(pos) = path.rfind('/') {
677 if let Some(dot_pos) = path[pos..].find('.') {
678 return path[pos + 1..pos + dot_pos].to_string();
679 }
680 }
681 path.to_string()
682 }
683
684 fn generate_outputs(&self, report: &CoverageReport) -> Result<()> {
686 fs::create_dir_all(&self.config.output_directory).map_err(|e| {
687 SklearsError::InvalidOperation(format!("Failed to create output directory: {e}"))
688 })?;
689
690 for format in &self.config.output_formats {
691 match format.as_str() {
692 "json" => self.generate_json_output(report)?,
693 "html" => self.generate_html_output(report)?,
694 "xml" => self.generate_xml_output(report)?,
695 "text" => self.generate_text_output(report)?,
696 _ => {
697 eprintln!("Warning: Unknown output format '{format}'");
698 }
699 }
700 }
701
702 Ok(())
703 }
704
705 fn generate_json_output(&self, report: &CoverageReport) -> Result<()> {
707 let json = serde_json::to_string_pretty(report).map_err(|e| {
708 SklearsError::InvalidOperation(format!("Failed to serialize JSON: {e}"))
709 })?;
710
711 let path = self.config.output_directory.join("coverage.json");
712 fs::write(path, json).map_err(|e| {
713 SklearsError::InvalidOperation(format!("Failed to write JSON output: {e}"))
714 })
715 }
716
717 fn generate_html_output(&self, report: &CoverageReport) -> Result<()> {
719 let html = self.generate_html_content(report);
720 let path = self.config.output_directory.join("coverage.html");
721 fs::write(path, html).map_err(|e| {
722 SklearsError::InvalidOperation(format!("Failed to write HTML output: {e}"))
723 })
724 }
725
726 fn generate_xml_output(&self, report: &CoverageReport) -> Result<()> {
728 let xml = self.generate_xml_content(report);
729 let path = self.config.output_directory.join("coverage.xml");
730 fs::write(path, xml)
731 .map_err(|e| SklearsError::InvalidOperation(format!("Failed to write XML output: {e}")))
732 }
733
734 fn generate_text_output(&self, report: &CoverageReport) -> Result<()> {
736 let text = report.summary();
737 let path = self.config.output_directory.join("coverage.txt");
738 fs::write(path, text).map_err(|e| {
739 SklearsError::InvalidOperation(format!("Failed to write text output: {e}"))
740 })
741 }
742
743 fn generate_html_content(&self, report: &CoverageReport) -> String {
745 format!(
746 r#"<!DOCTYPE html>
747<html>
748<head>
749 <title>Code Coverage Report</title>
750 <style>
751 body {{ font-family: Arial, sans-serif; margin: 40px; }}
752 .summary {{ background: #f5f5f5; padding: 20px; border-radius: 5px; }}
753 .metric {{ margin: 10px 0; }}
754 .pass {{ color: green; }}
755 .fail {{ color: red; }}
756 .warning {{ color: orange; }}
757 table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
758 th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
759 th {{ background-color: #f2f2f2; }}
760 .coverage-bar {{
761 width: 100px;
762 height: 20px;
763 background: #ddd;
764 border-radius: 10px;
765 overflow: hidden;
766 }}
767 .coverage-fill {{
768 height: 100%;
769 background: linear-gradient(to right, #ff4444, #ffaa00, #44ff44);
770 }}
771 </style>
772</head>
773<body>
774 <h1>Code Coverage Report</h1>
775
776 <div class="summary">
777 <h2>Summary</h2>
778 <div class="metric">Overall Coverage: <strong>{:.1}%</strong></div>
779 <div class="metric">Lines: {}/{} ({:.1}%)</div>
780 <div class="metric">Functions: {}/{} ({:.1}%)</div>
781 <div class="metric">Branches: {}/{} ({:.1}%)</div>
782 <div class="metric">Quality Gates: <span class="{}">{}</span></div>
783 </div>
784
785 <h2>Quality Gates</h2>
786 {}
787
788 <h2>Recommendations</h2>
789 {}
790
791 <p><em>Generated at: {}</em></p>
792</body>
793</html>"#,
794 report.overall_coverage(),
795 report.overall_summary.lines_covered,
796 report.overall_summary.lines_total,
797 report.overall_summary.line_coverage(),
798 report.overall_summary.functions_covered,
799 report.overall_summary.functions_total,
800 report.overall_summary.function_coverage(),
801 report.overall_summary.branches_covered,
802 report.overall_summary.branches_total,
803 report.overall_summary.branch_coverage(),
804 if report.quality_gates.passed {
805 "pass"
806 } else {
807 "fail"
808 },
809 if report.quality_gates.passed {
810 "PASSED"
811 } else {
812 "FAILED"
813 },
814 if report.quality_gates.failures.is_empty() {
815 "<p class=\"pass\">All quality gates passed!</p>".to_string()
816 } else {
817 format!(
818 "<ul>{}</ul>",
819 report
820 .quality_gates
821 .failures
822 .iter()
823 .map(|f| format!(
824 "<li class=\"fail\">{}: Expected {:.1}%, got {:.1}%</li>",
825 f.rule, f.expected, f.actual
826 ))
827 .collect::<Vec<_>>()
828 .join("")
829 )
830 },
831 if report.recommendations.is_empty() {
832 "<p>No recommendations at this time.</p>".to_string()
833 } else {
834 format!(
835 "<ul>{}</ul>",
836 report
837 .recommendations
838 .iter()
839 .map(|r| format!("<li>{}</li>", r.description))
840 .collect::<Vec<_>>()
841 .join("")
842 )
843 },
844 chrono::DateTime::from_timestamp(report.timestamp as i64, 0)
845 .unwrap_or_default()
846 .format("%Y-%m-%d %H:%M:%S UTC")
847 )
848 }
849
850 fn generate_xml_content(&self, report: &CoverageReport) -> String {
852 format!(
853 r#"<?xml version="1.0" encoding="UTF-8"?>
854<coverage timestamp="{}" lines-covered="{}" lines-valid="{}" line-rate="{:.4}">
855 <packages>
856 <package name="sklears" line-rate="{:.4}" branch-rate="{:.4}">
857 <classes>
858 {}
859 </classes>
860 </package>
861 </packages>
862</coverage>"#,
863 report.timestamp,
864 report.overall_summary.lines_covered,
865 report.overall_summary.lines_total,
866 report.overall_summary.line_coverage() / 100.0,
867 report.overall_summary.line_coverage() / 100.0,
868 report.overall_summary.branch_coverage() / 100.0,
869 report
870 .module_summaries
871 .iter()
872 .map(|(name, summary)| {
873 format!(
874 r#"<class name="{}" line-rate="{:.4}" branch-rate="{:.4}"></class>"#,
875 name,
876 summary.line_coverage() / 100.0,
877 summary.branch_coverage() / 100.0
878 )
879 })
880 .collect::<Vec<_>>()
881 .join("\n ")
882 )
883 }
884}
885
886#[derive(Debug)]
888pub struct CoverageCI {
889 config: CIDConfig,
890}
891
892#[derive(Debug, Clone)]
894pub struct CIDConfig {
895 pub pr_coverage_threshold: f64,
896 pub diff_coverage_threshold: f64,
897 pub failure_on_regression: bool,
898 pub post_results_to_pr: bool,
899 pub badge_generation: bool,
900}
901
902impl Default for CIDConfig {
903 fn default() -> Self {
904 Self {
905 pr_coverage_threshold: 80.0,
906 diff_coverage_threshold: 90.0,
907 failure_on_regression: true,
908 post_results_to_pr: false,
909 badge_generation: true,
910 }
911 }
912}
913
914impl CIDConfig {
915 pub fn new() -> Self {
916 Self::default()
917 }
918
919 pub fn with_pr_coverage_threshold(mut self, threshold: f64) -> Self {
920 self.pr_coverage_threshold = threshold;
921 self
922 }
923
924 pub fn with_diff_coverage_threshold(mut self, threshold: f64) -> Self {
925 self.diff_coverage_threshold = threshold;
926 self
927 }
928
929 pub fn with_failure_on_regression(mut self, enabled: bool) -> Self {
930 self.failure_on_regression = enabled;
931 self
932 }
933}
934
935#[derive(Debug)]
937pub struct CICoverageResult {
938 pub coverage: f64,
939 pub diff_coverage: Option<f64>,
940 pub passed: bool,
941 pub failures: Vec<String>,
942}
943
944impl CoverageCI {
945 pub fn new(config: CIDConfig) -> Self {
946 Self { config }
947 }
948
949 pub fn run_coverage_check(&self) -> std::result::Result<CICoverageResult, Vec<String>> {
951 let mut failures = Vec::new();
952
953 let coverage_config =
955 CoverageConfig::new().with_minimum_coverage(self.config.pr_coverage_threshold);
956
957 let mut collector = CoverageCollector::new(coverage_config);
958 let report = match collector.collect_and_analyze() {
959 Ok(report) => report,
960 Err(e) => {
961 failures.push(format!("Failed to collect coverage: {e}"));
962 return Err(failures);
963 }
964 };
965
966 let coverage = report.overall_coverage();
967
968 if coverage < self.config.pr_coverage_threshold {
970 failures.push(format!(
971 "Coverage {:.1}% below PR threshold {:.1}%",
972 coverage, self.config.pr_coverage_threshold
973 ));
974 }
975
976 if !report.meets_quality_gates() {
978 for failure in &report.quality_gates.failures {
979 failures.push(format!(
980 "{}: {:.1}% < {:.1}%",
981 failure.rule, failure.actual, failure.expected
982 ));
983 }
984 }
985
986 let result = CICoverageResult {
987 coverage,
988 diff_coverage: None, passed: failures.is_empty(),
990 failures: failures.clone(),
991 };
992
993 if failures.is_empty() {
994 Ok(result)
995 } else {
996 Err(failures)
997 }
998 }
999}
1000
1001#[allow(non_snake_case)]
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn test_coverage_config_creation() {
1008 let config = CoverageConfig::new()
1009 .with_minimum_coverage(85.0)
1010 .with_output_format(vec!["json", "html"])
1011 .with_exclude_patterns(vec!["tests/*"]);
1012
1013 assert_eq!(config.minimum_coverage, 85.0);
1014 assert_eq!(config.output_formats, vec!["json", "html"]);
1015 assert_eq!(config.exclude_patterns, vec!["tests/*"]);
1016 }
1017
1018 #[test]
1019 fn test_coverage_summary_calculations() {
1020 let summary = CoverageSummary {
1021 lines_covered: 80,
1022 lines_total: 100,
1023 functions_covered: 9,
1024 functions_total: 10,
1025 branches_covered: 15,
1026 branches_total: 20,
1027 };
1028
1029 assert_eq!(summary.line_coverage(), 80.0);
1030 assert_eq!(summary.function_coverage(), 90.0);
1031 assert_eq!(summary.branch_coverage(), 75.0);
1032 }
1033
1034 #[test]
1035 fn test_quality_gates_evaluation() {
1036 let config = CoverageConfig::new().with_minimum_coverage(85.0);
1037 let collector = CoverageCollector::new(config);
1038
1039 let summary = CoverageSummary {
1040 lines_covered: 80,
1041 lines_total: 100,
1042 functions_covered: 8,
1043 functions_total: 10,
1044 branches_covered: 12,
1045 branches_total: 15,
1046 };
1047
1048 let result = collector.evaluate_quality_gates(&summary);
1049 assert!(!result.passed);
1050 assert_eq!(result.failures.len(), 1);
1051 assert_eq!(result.failures[0].rule, "Minimum overall coverage");
1052 }
1053
1054 #[test]
1055 fn test_coverage_collector_creation() {
1056 let config = CoverageConfig::new();
1057 let collector = CoverageCollector::new(config);
1058 assert!(collector.collected_data.is_none());
1059 }
1060
1061 #[test]
1062 fn test_ci_config_creation() {
1063 let config = CIDConfig::new()
1064 .with_pr_coverage_threshold(85.0)
1065 .with_diff_coverage_threshold(95.0)
1066 .with_failure_on_regression(false);
1067
1068 assert_eq!(config.pr_coverage_threshold, 85.0);
1069 assert_eq!(config.diff_coverage_threshold, 95.0);
1070 assert!(!config.failure_on_regression);
1071 }
1072
1073 #[test]
1074 fn test_coverage_ci_creation() {
1075 let config = CIDConfig::new();
1076 let ci = CoverageCI::new(config);
1077 assert_eq!(ci.config.pr_coverage_threshold, 80.0);
1078 }
1079
1080 #[test]
1081 fn test_recommendation_priority_ordering() {
1082 let critical = RecommendationPriority::Critical;
1083 let high = RecommendationPriority::High;
1084 let medium = RecommendationPriority::Medium;
1085 let low = RecommendationPriority::Low;
1086
1087 assert!(critical < high);
1088 assert!(high < medium);
1089 assert!(medium < low);
1090 }
1091}