use std::collections::{BTreeMap, BTreeSet};
use crate::coverage::{self, Outcome, RustThresholds};
pub(crate) fn evaluate_patch_rust(
changed: &BTreeMap<String, BTreeSet<u64>>,
detail: &BTreeMap<String, coverage::RustPatchCoverage>,
thresholds: RustThresholds,
) -> Outcome {
let (mut r_cov, mut r_tot) = (0u64, 0u64);
let (mut l_cov, mut l_tot) = (0u64, 0u64);
for (file, lines) in changed {
let Some(cov) = detail.get(file) else {
continue;
};
for &(start, end, covered) in &cov.regions {
if (start..=end).any(|line| lines.contains(&line)) {
r_tot += 1;
if covered {
r_cov += 1;
}
}
}
for &line in lines {
let mut measured = false;
let mut covered_here = false;
for &(start, end, covered) in &cov.regions {
if start <= line && line <= end {
measured = true;
covered_here |= covered;
}
}
if measured {
l_tot += 1;
if covered_here {
l_cov += 1;
}
}
}
}
let mut checks: Vec<(&str, u64, u64, u8)> = Vec::new();
if let Some(regions) = thresholds.regions {
checks.push(("regions", r_cov, r_tot, regions));
}
checks.push(("lines", l_cov, l_tot, thresholds.lines));
let mut shortfalls = Vec::new();
for (name, covered, total, required) in checks {
if u128::from(covered) * 100 < u128::from(total) * u128::from(required) {
let actual = 100.0 * covered as f64 / total as f64;
shortfalls.push(format!("{name} {actual:.2}% < {required}%"));
}
}
if shortfalls.is_empty() {
Outcome::Pass
} else {
Outcome::Fail(format!(
"coverage below thresholds: {}",
shortfalls.join(", ")
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn changed(entries: &[(&str, &[u64])]) -> BTreeMap<String, BTreeSet<u64>> {
entries
.iter()
.map(|(path, lines)| (path.to_string(), lines.iter().copied().collect()))
.collect()
}
use coverage::RustPatchCoverage;
fn rust_detail(entries: &[(&str, RustPatchCoverage)]) -> BTreeMap<String, RustPatchCoverage> {
entries
.iter()
.map(|(path, cov)| (path.to_string(), cov.clone()))
.collect()
}
const RUST_FLOOR_80: RustThresholds = RustThresholds {
regions: Some(80),
lines: 80,
functions: None,
branch: None,
};
#[test]
fn rust_patch_a_fully_covered_diff_passes() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(1, 1, true), (2, 2, true)],
},
)]);
assert_eq!(
evaluate_patch_rust(&changed(&[("w.rs", &[1, 2])]), &detail, RUST_FLOOR_80),
Outcome::Pass
);
}
#[test]
fn rust_patch_below_floor_fails_and_names_the_metrics() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(1, 1, true), (2, 2, true), (3, 3, true), (4, 4, false)],
},
)]);
let out = evaluate_patch_rust(&changed(&[("w.rs", &[1, 2, 3, 4])]), &detail, RUST_FLOOR_80);
assert!(
matches!(&out, Outcome::Fail(m)
if m.contains("regions 75.00% < 80%")
&& m.contains("lines 75.00% < 80%")),
"got: {out:?}"
);
}
#[test]
fn rust_patch_the_same_diff_clears_a_lower_floor() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(1, 1, true), (2, 2, true), (3, 3, true), (4, 4, false)],
},
)]);
let floor_70 = RustThresholds {
regions: Some(70),
lines: 70,
functions: None,
branch: None,
};
assert_eq!(
evaluate_patch_rust(&changed(&[("w.rs", &[1, 2, 3, 4])]), &detail, floor_70),
Outcome::Pass
);
}
#[test]
fn rust_patch_passes_at_the_exact_floor() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![
(1, 1, true),
(2, 2, true),
(3, 3, true),
(4, 4, true),
(5, 5, false),
],
},
)]);
assert_eq!(
evaluate_patch_rust(
&changed(&[("w.rs", &[1, 2, 3, 4, 5])]),
&detail,
RUST_FLOOR_80
),
Outcome::Pass
);
}
#[test]
fn rust_patch_skips_the_region_check_when_regions_is_opt_out() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(1, 4, true), (4, 4, false)],
},
)]);
let lines_only = RustThresholds {
regions: None,
lines: 100,
functions: None,
branch: None,
};
assert_eq!(
evaluate_patch_rust(&changed(&[("w.rs", &[1, 2, 3, 4])]), &detail, lines_only),
Outcome::Pass
);
}
#[test]
fn rust_patch_an_uncovered_region_on_a_changed_line_fails_both_metrics() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(5, 5, false)],
},
)]);
let out = evaluate_patch_rust(&changed(&[("w.rs", &[5])]), &detail, RUST_FLOOR_80);
assert!(
matches!(&out, Outcome::Fail(m)
if m.contains("regions 0.00% < 80%") && m.contains("lines 0.00% < 80%")),
"got: {out:?}"
);
}
#[test]
fn rust_patch_a_changed_file_absent_from_coverage_is_skipped() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(1, 1, true)],
},
)]);
assert_eq!(
evaluate_patch_rust(&changed(&[("other.rs", &[1, 2])]), &detail, RUST_FLOOR_80),
Outcome::Pass
);
}
#[test]
fn rust_patch_a_comment_only_diff_passes() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(1, 1, true), (2, 2, true)],
},
)]);
assert_eq!(
evaluate_patch_rust(&changed(&[("w.rs", &[9, 10])]), &detail, RUST_FLOOR_80),
Outcome::Pass
);
}
#[test]
fn rust_patch_an_empty_diff_passes() {
assert_eq!(
evaluate_patch_rust(&changed(&[]), &BTreeMap::new(), RUST_FLOOR_80),
Outcome::Pass
);
}
#[test]
fn rust_patch_a_multiline_region_counts_when_any_of_its_lines_changed() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(3, 5, false)],
},
)]);
let out = evaluate_patch_rust(&changed(&[("w.rs", &[4])]), &detail, RUST_FLOOR_80);
assert!(
matches!(&out, Outcome::Fail(m)
if m.contains("regions 0.00% < 80%") && m.contains("lines 0.00% < 80%")),
"got: {out:?}"
);
}
#[test]
fn rust_patch_a_line_covered_by_any_region_is_covered() {
let detail = rust_detail(&[(
"w.rs",
RustPatchCoverage {
regions: vec![(4, 4, false), (4, 6, true)],
},
)]);
let out = evaluate_patch_rust(&changed(&[("w.rs", &[4])]), &detail, RUST_FLOOR_80);
assert!(
matches!(&out, Outcome::Fail(m)
if m.contains("regions 50.00% < 80%") && !m.contains("lines")),
"got: {out:?}"
);
}
}