use std::collections::HashMap;
use tracing::{info, warn};
use crate::{
config::ReviewConfig,
coverage::{CoverageVerdictContrib, evaluate_coverage, new_code_coverage, parse_lcov_file},
};
pub async fn load_coverage_contrib(
config: &ReviewConfig,
diff: &str,
) -> Option<CoverageVerdictContrib> {
if !config.coverage.enabled {
return None;
}
let path = config.coverage.lcov_path.as_ref()?;
let report = match parse_lcov_file(path) {
Ok(r) => r,
Err(e) => {
warn!(
path = %path.display(),
"coverage: failed to load LCOV file (proceeding without coverage): {e}"
);
return None;
}
};
let added_lines = extract_added_lines_from_diff(diff);
let new_code_pct = new_code_coverage(&report, &added_lines);
let contrib = evaluate_coverage(&config.coverage, &report, new_code_pct, None);
info!(
net_pct = report.net_pct,
new_code_pct = new_code_pct.unwrap_or(-1.0),
floor = ?contrib.floor,
"coverage evaluation complete"
);
Some(contrib)
}
pub fn extract_added_lines_from_diff(diff: &str) -> HashMap<String, Vec<u32>> {
let mut result: HashMap<String, Vec<u32>> = HashMap::new();
let mut current_file: Option<String> = None;
let mut current_line: u32 = 0;
for line in diff.lines() {
if let Some(stripped) = line.strip_prefix("+++ b/") {
let path = stripped.to_string();
current_file = Some(path);
current_line = 0;
} else if line.starts_with("--- ") || line.starts_with("+++ ") {
} else if line.starts_with("@@ ") {
if let Some(plus_pos) = line.find('+') {
let rest = &line[plus_pos + 1..];
let end = rest.find([',', ' ']).unwrap_or(rest.len());
if let Ok(n) = rest[..end].parse::<u32>() {
current_line = n.saturating_sub(1);
}
}
} else if line.starts_with('+') {
current_line += 1;
if let Some(ref file) = current_file {
result.entry(file.clone()).or_default().push(current_line);
}
} else if line.starts_with('-') {
} else {
current_line += 1;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_added_lines_basic() {
let diff = "\
--- a/src/foo.rs
+++ b/src/foo.rs
@@ -1,2 +1,3 @@
fn foo() {
+ let x = 1;
}
";
let added = extract_added_lines_from_diff(diff);
let lines = added.get("src/foo.rs").expect("src/foo.rs must be present");
assert_eq!(lines, &[2], "added line must be at new-file line 2");
}
#[test]
fn extract_added_lines_multi_file() {
let diff = "\
--- a/src/a.rs
+++ b/src/a.rs
@@ -1,1 +1,2 @@
fn a() {}
+fn b() {}
--- a/src/b.rs
+++ b/src/b.rs
@@ -1,1 +1,2 @@
fn c() {}
+fn d() {}
";
let added = extract_added_lines_from_diff(diff);
assert!(added.contains_key("src/a.rs"), "src/a.rs must be present");
assert!(added.contains_key("src/b.rs"), "src/b.rs must be present");
assert_eq!(added["src/a.rs"], [2]);
assert_eq!(added["src/b.rs"], [2]);
}
#[test]
fn extract_added_lines_hunk_restart() {
let diff = "\
--- a/src/foo.rs
+++ b/src/foo.rs
@@ -10,2 +10,3 @@
existing_line();
+new_line();
another_existing();
";
let added = extract_added_lines_from_diff(diff);
let lines = added.get("src/foo.rs").expect("must be present");
assert_eq!(lines, &[11]);
}
#[test]
fn extract_added_lines_empty() {
let added = extract_added_lines_from_diff("");
assert!(added.is_empty(), "empty diff must produce empty map");
}
}