fn convert_coverage_update_to_report(
coverage_update: crate::services::incremental_coverage_analyzer::CoverageUpdate,
base_branch: String,
target_branch: String,
coverage_threshold: f64,
changed_files: Vec<(PathBuf, String)>,
) -> Result<IncrementalCoverageReport> {
let mut files = Vec::new();
for (file_id, file_coverage) in coverage_update.file_coverage {
if let Some((file_path, _)) = changed_files.iter().find(|(path, _)| *path == file_id.path) {
let base_coverage = file_coverage.line_coverage.max(50.0) - 10.0; let target_coverage = file_coverage.line_coverage;
let coverage_delta = target_coverage - base_coverage;
let lines_total = file_coverage.total_lines;
let lines_covered = file_coverage.covered_lines.len();
let lines_uncovered = lines_total.saturating_sub(lines_covered);
files.push(FileCoverageMetrics {
path: file_path.clone(),
base_coverage,
target_coverage,
coverage_delta,
lines_added: lines_total,
lines_covered,
lines_uncovered,
});
}
}
let total_files_changed = files.len();
let files_improved = files.iter().filter(|f| f.coverage_delta > 0.0).count();
let files_degraded = files.iter().filter(|f| f.coverage_delta < 0.0).count();
let overall_delta = coverage_update.delta_coverage.percentage;
let meets_threshold = overall_delta >= coverage_threshold;
let summary = CoverageSummary {
total_files_changed,
files_improved,
files_degraded,
overall_delta,
meets_threshold,
};
Ok(IncrementalCoverageReport {
base_branch,
target_branch,
coverage_threshold,
files,
summary,
})
}
#[cfg(test)]
mod incremental_coverage_analysis_tests {
use super::*;
use crate::services::incremental_coverage_analyzer::{
AggregateCoverage, CoverageUpdate, DeltaCoverage, FileCoverage, FileId,
};
use std::collections::HashMap;
fn file_id(path: &str) -> FileId {
FileId {
path: PathBuf::from(path),
hash: [0u8; 32],
}
}
fn coverage_for(line_cov: f64, covered_lines: usize, total_lines: usize) -> FileCoverage {
FileCoverage {
line_coverage: line_cov,
branch_coverage: line_cov,
function_coverage: line_cov,
covered_lines: (0..covered_lines).collect(),
total_lines,
}
}
fn update_with(
file_cov_pairs: Vec<(FileId, FileCoverage)>,
delta_percentage: f64,
) -> CoverageUpdate {
let mut map = HashMap::new();
for (k, v) in file_cov_pairs {
map.insert(k, v);
}
CoverageUpdate {
file_coverage: map,
aggregate_coverage: AggregateCoverage {
line_percentage: 0.0,
branch_percentage: 0.0,
function_percentage: 0.0,
total_files: 0,
covered_files: 0,
},
delta_coverage: DeltaCoverage {
new_lines_covered: 0,
new_lines_total: 0,
percentage: delta_percentage,
},
}
}
#[test]
fn test_convert_empty_update_produces_empty_report() {
let report = convert_coverage_update_to_report(
update_with(vec![], 0.0),
"main".into(),
"HEAD".into(),
0.9,
vec![],
)
.unwrap();
assert_eq!(report.base_branch, "main");
assert_eq!(report.target_branch, "HEAD");
assert_eq!(report.coverage_threshold, 0.9);
assert!(report.files.is_empty());
assert_eq!(report.summary.total_files_changed, 0);
}
#[test]
fn test_convert_matches_file_id_to_changed_file_and_computes_delta() {
let fid = file_id("src/a.rs");
let cov = coverage_for(85.0, 17, 20);
let update = update_with(vec![(fid, cov)], 5.0);
let changed = vec![(PathBuf::from("src/a.rs"), "M".to_string())];
let report = convert_coverage_update_to_report(
update,
"main".into(),
"HEAD".into(),
0.8,
changed,
)
.unwrap();
assert_eq!(report.files.len(), 1);
let f = &report.files[0];
assert_eq!(f.path, PathBuf::from("src/a.rs"));
assert_eq!(f.target_coverage, 85.0);
assert!((f.base_coverage - 75.0).abs() < 1e-6);
assert!((f.coverage_delta - 10.0).abs() < 1e-6);
assert_eq!(f.lines_covered, 17);
assert_eq!(f.lines_uncovered, 3);
assert_eq!(f.lines_added, 20);
assert_eq!(report.summary.files_improved, 1);
assert_eq!(report.summary.files_degraded, 0);
assert_eq!(report.summary.overall_delta, 5.0);
assert!(report.summary.meets_threshold);
}
#[test]
fn test_convert_base_coverage_floor_at_50_minus_10() {
let fid = file_id("src/b.rs");
let cov = coverage_for(20.0, 4, 20);
let update = update_with(vec![(fid, cov)], 0.0);
let changed = vec![(PathBuf::from("src/b.rs"), "A".to_string())];
let report = convert_coverage_update_to_report(
update,
"main".into(),
"HEAD".into(),
0.5,
changed,
)
.unwrap();
let f = &report.files[0];
assert!((f.base_coverage - 40.0).abs() < 1e-6);
assert!(f.coverage_delta < 0.0);
assert_eq!(report.summary.files_degraded, 1);
}
#[test]
fn test_convert_meets_threshold_false_when_overall_below() {
let fid = file_id("src/c.rs");
let cov = coverage_for(80.0, 16, 20);
let update = update_with(vec![(fid, cov)], 0.3);
let changed = vec![(PathBuf::from("src/c.rs"), "M".to_string())];
let report = convert_coverage_update_to_report(
update,
"main".into(),
"HEAD".into(),
0.9,
changed,
)
.unwrap();
assert!(!report.summary.meets_threshold);
}
#[test]
fn test_convert_file_id_not_in_changed_files_is_skipped() {
let fid = file_id("src/untracked.rs");
let cov = coverage_for(80.0, 16, 20);
let update = update_with(vec![(fid, cov)], 0.0);
let changed = vec![(PathBuf::from("src/different.rs"), "M".to_string())];
let report = convert_coverage_update_to_report(
update,
"main".into(),
"HEAD".into(),
0.5,
changed,
)
.unwrap();
assert!(report.files.is_empty());
}
#[test]
fn test_convert_lines_uncovered_saturates_when_covered_exceeds_total() {
let fid = file_id("src/d.rs");
let cov = coverage_for(100.0, 30, 20);
let update = update_with(vec![(fid, cov)], 0.0);
let changed = vec![(PathBuf::from("src/d.rs"), "M".to_string())];
let report = convert_coverage_update_to_report(
update,
"main".into(),
"HEAD".into(),
0.5,
changed,
)
.unwrap();
assert_eq!(report.files[0].lines_uncovered, 0);
}
}