use std::collections::{BTreeMap, BTreeSet};
use super::diff::{DiffModel, FileDiff};
use super::markers::{FileMarkers, MarkerKind, Region};
use super::model::{CoverageReport, FileCoverage};
type BaseToHead<'a> = Box<dyn Fn(u32) -> Option<u32> + 'a>;
#[derive(Debug, Clone, Default)]
pub struct Markers {
pub head: BTreeMap<String, FileMarkers>,
pub base: BTreeMap<String, FileMarkers>,
}
impl Markers {
pub fn is_empty(&self) -> bool {
self.head.values().all(FileMarkers::is_empty)
&& self.base.values().all(FileMarkers::is_empty)
}
fn tolerated(&self, path: &str) -> Option<&BTreeSet<u32>> {
self.head
.get(path)
.map(|m| &m.tolerated)
.filter(|t| !t.is_empty())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppliedMarker {
pub path: String,
pub kind: MarkerKind,
pub side: MarkerSide,
pub start: u32,
pub end: u32,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkerSide {
Both,
Head,
Base,
}
impl MarkerSide {
pub fn as_str(self) -> &'static str {
match self {
Self::Both => "both",
Self::Head => "head",
Self::Base => "base",
}
}
}
const NOTABLE_UNCHANGED_LINES: u64 = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DiffScope {
#[default]
DiffOnly,
All,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PatchCoverage {
pub covered: u64,
pub uncovered: u64,
}
impl PatchCoverage {
pub fn total(&self) -> u64 {
self.covered + self.uncovered
}
pub fn percent(&self) -> Option<f64> {
let total = self.total();
if total == 0 {
None
} else {
Some(self.covered as f64 / total as f64 * 100.0)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilePatch {
pub path: String,
pub patch: PatchCoverage,
pub uncovered_lines: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FileDelta {
pub path: String,
pub before: Option<f64>,
pub after: Option<f64>,
pub after_effective: Option<f64>,
}
impl FileDelta {
pub fn new(path: impl Into<String>, before: Option<f64>, after: Option<f64>) -> Self {
Self {
path: path.into(),
before,
after,
after_effective: after,
}
}
pub fn delta(&self) -> Option<f64> {
match (self.before, self.after_effective) {
(Some(b), Some(a)) => Some(a - b),
(Some(b), None) => Some(0.0 - b),
_ => None,
}
}
pub fn is_masked(&self) -> bool {
self.after_effective != self.after
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndirectChange {
pub path: String,
pub base_line: u32,
pub head_line: u32,
pub became_covered: bool,
}
#[derive(Debug, Clone, Default)]
pub struct CoverageDiff {
pub patch: PatchCoverage,
pub file_patches: Vec<FilePatch>,
pub uncovered_new_lines: Vec<(String, u32)>,
pub has_baseline: bool,
pub total_after: Option<f64>,
pub total_after_effective: Option<f64>,
pub total_before: Option<f64>,
pub file_deltas: Vec<FileDelta>,
pub notable_unchanged: Vec<FileDelta>,
pub indirect: Vec<IndirectChange>,
pub markers: Vec<AppliedMarker>,
}
impl CoverageDiff {
pub fn indirect_newly_covered(&self) -> usize {
self.indirect.iter().filter(|c| c.became_covered).count()
}
pub fn indirect_newly_uncovered(&self) -> usize {
self.indirect.iter().filter(|c| !c.became_covered).count()
}
}
pub fn analyze(
head: &CoverageReport,
diff: &DiffModel,
baseline: Option<&CoverageReport>,
scope: DiffScope,
) -> CoverageDiff {
analyze_with_markers(head, diff, baseline, scope, &Markers::default())
}
pub fn analyze_with_markers(
head: &CoverageReport,
diff: &DiffModel,
baseline: Option<&CoverageReport>,
scope: DiffScope,
markers: &Markers,
) -> CoverageDiff {
let mut result = CoverageDiff {
total_after: head.percent(),
has_baseline: baseline.is_some(),
markers: applied_markers(markers),
..Default::default()
};
patch_coverage(head, diff, &mut result);
if let Some(baseline) = baseline {
result.total_before = baseline.percent();
project_delta(head, baseline, diff, scope, markers, &mut result);
indirect_changes(head, baseline, diff, scope, markers, &mut result);
}
result
}
fn applied_markers(markers: &Markers) -> Vec<AppliedMarker> {
let same_region = |a: &Region, b: &Region| a.kind == b.kind && a.reason == b.reason;
let mut applied: Vec<AppliedMarker> = Vec::new();
for (path, file) in &markers.head {
for region in &file.regions {
let same_at_base = markers
.base
.get(path)
.is_some_and(|base| base.regions.iter().any(|other| same_region(other, region)));
applied.push(AppliedMarker {
path: path.clone(),
kind: region.kind,
side: if same_at_base {
MarkerSide::Both
} else {
MarkerSide::Head
},
start: region.start,
end: region.end,
reason: region.reason.clone(),
});
}
}
for (path, file) in &markers.base {
for region in &file.regions {
let seen_at_head = markers
.head
.get(path)
.is_some_and(|head| head.regions.iter().any(|other| same_region(other, region)));
if seen_at_head {
continue;
}
applied.push(AppliedMarker {
path: path.clone(),
kind: region.kind,
side: MarkerSide::Base,
start: region.start,
end: region.end,
reason: region.reason.clone(),
});
}
}
applied.sort_by(|a, b| a.path.cmp(&b.path).then(a.start.cmp(&b.start)));
applied
}
fn tolerated_substitutions(
base_file: &FileCoverage,
map: &BaseToHead<'_>,
tolerated: &BTreeSet<u32>,
) -> BTreeMap<u32, u64> {
let mut substitutions = BTreeMap::new();
for (&base_line, &base_hits) in &base_file.lines {
let Some(head_line) = map(base_line) else {
continue;
};
if tolerated.contains(&head_line) {
substitutions.insert(head_line, base_hits);
}
}
substitutions
}
fn effective_covered(file: &FileCoverage, substitutions: &BTreeMap<u32, u64>) -> u64 {
file.lines
.iter()
.filter(|(line, hits)| {
let effective = substitutions.get(line).unwrap_or(hits);
*effective > 0
})
.count() as u64
}
fn patch_coverage(head: &CoverageReport, diff: &DiffModel, result: &mut CoverageDiff) {
for file in diff.files.values() {
let mut patch = PatchCoverage::default();
let mut uncovered_lines = Vec::new();
for &line in &file.added {
match head.hits(&file.new_path, line) {
Some(h) if h > 0 => patch.covered += 1,
Some(_) => {
patch.uncovered += 1;
uncovered_lines.push(line);
}
None => {}
}
}
if patch.total() == 0 {
continue;
}
result.patch.covered += patch.covered;
result.patch.uncovered += patch.uncovered;
for &line in &uncovered_lines {
result
.uncovered_new_lines
.push((file.new_path.clone(), line));
}
result.file_patches.push(FilePatch {
path: file.new_path.clone(),
patch,
uncovered_lines,
});
}
result.file_patches.sort_by(|a, b| a.path.cmp(&b.path));
result
.uncovered_new_lines
.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
}
fn project_delta(
head: &CoverageReport,
baseline: &CoverageReport,
diff: &DiffModel,
scope: DiffScope,
markers: &Markers,
result: &mut CoverageDiff,
) {
let by_old_path = index_by_old_path(diff);
let mut effective_covered_total = 0_u64;
for (path, file) in &head.files {
let substitutions = markers
.tolerated(path)
.and_then(|tolerated| {
let (base_path, map) = base_side(path, diff, &by_old_path)?;
let base_file = baseline.files.get(&base_path)?;
Some(tolerated_substitutions(base_file, &map, tolerated))
})
.unwrap_or_default();
let covered_after = file.covered_lines();
let covered_effective = if substitutions.is_empty() {
covered_after
} else {
effective_covered(file, &substitutions)
};
effective_covered_total += covered_effective;
let total = file.total_lines();
let percent = |covered: u64| (total > 0).then(|| covered as f64 / total as f64 * 100.0);
let delta = FileDelta {
path: path.clone(),
before: baseline.files.get(path).and_then(FileCoverage::percent),
after: percent(covered_after),
after_effective: percent(covered_effective),
};
if scope == DiffScope::All || diff.files.contains_key(path) {
result.file_deltas.push(delta);
continue;
}
let covered_before = baseline
.files
.get(path)
.map_or(0, FileCoverage::covered_lines);
let net = covered_effective.abs_diff(covered_before);
if net >= NOTABLE_UNCHANGED_LINES {
result.notable_unchanged.push(delta);
}
}
let total_lines = head.total_lines();
result.total_after_effective =
(total_lines > 0).then(|| effective_covered_total as f64 / total_lines as f64 * 100.0);
result.file_deltas.sort_by(|a, b| a.path.cmp(&b.path));
result.notable_unchanged.sort_by(|a, b| a.path.cmp(&b.path));
}
fn index_by_old_path(diff: &DiffModel) -> BTreeMap<&str, &FileDiff> {
diff.files
.values()
.filter_map(|f| f.old_path.as_deref().map(|p| (p, f)))
.collect()
}
fn base_side<'a>(
head_path: &str,
diff: &'a DiffModel,
by_old_path: &BTreeMap<&'a str, &'a FileDiff>,
) -> Option<(String, BaseToHead<'a>)> {
match diff.files.get(head_path) {
Some(fd) if fd.is_new => None,
Some(fd) => {
let old_path = fd.old_path.clone()?;
Some((old_path, Box::new(move |l| fd.map_base_to_head(l))))
}
None => {
let _ = by_old_path;
Some((head_path.to_string(), Box::new(Some)))
}
}
}
fn indirect_changes(
head: &CoverageReport,
baseline: &CoverageReport,
diff: &DiffModel,
scope: DiffScope,
markers: &Markers,
result: &mut CoverageDiff,
) {
let by_old_path = index_by_old_path(diff);
for (base_path, base_file) in &baseline.files {
let (new_path, map): (&str, BaseToHead<'_>) =
if let Some(fd) = by_old_path.get(base_path.as_str()) {
let fd = *fd;
(
fd.new_path.as_str(),
Box::new(move |l| fd.map_base_to_head(l)),
)
} else if scope == DiffScope::All
&& head.files.contains_key(base_path)
&& !diff.files.contains_key(base_path)
{
(base_path.as_str(), Box::new(Some))
} else {
continue;
};
for (&base_line, &base_hits) in &base_file.lines {
let Some(head_line) = map(base_line) else {
continue;
};
let Some(head_hits) = head.hits(new_path, head_line) else {
continue;
};
if markers
.tolerated(new_path)
.is_some_and(|t| t.contains(&head_line))
{
continue;
}
let covered_before = base_hits > 0;
let covered_after = head_hits > 0;
if covered_before != covered_after {
result.indirect.push(IndirectChange {
path: new_path.to_string(),
base_line,
head_line,
became_covered: covered_after,
});
}
}
}
result
.indirect
.sort_by(|a, b| a.path.cmp(&b.path).then(a.head_line.cmp(&b.head_line)));
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::coverage::model::FileCoverage;
use std::collections::{BTreeMap, BTreeSet};
pub(super) fn report(files: &[(&str, &[(u32, u64)])]) -> CoverageReport {
let mut r = CoverageReport::new();
for (path, lines) in files {
let mut f = FileCoverage::new(*path);
for &(n, h) in *lines {
f.record(n, h);
}
r.insert(f);
}
r
}
pub(super) fn diff_added(path: &str, is_new: bool, added: &[u32]) -> DiffModel {
let old_path = if is_new { None } else { Some(path.to_string()) };
let fd = FileDiff::new(
path,
old_path,
is_new,
false,
added.iter().copied().collect::<BTreeSet<u32>>(),
BTreeSet::new(),
);
let mut files = BTreeMap::new();
files.insert(path.to_string(), fd);
DiffModel { files }
}
#[test]
fn patch_coverage_counts_added_lines_only() {
let head = report(&[("src/a.rs", &[(1, 1), (2, 1), (3, 0), (4, 1)])]);
let diff = diff_added("src/a.rs", false, &[2, 3]);
let out = analyze(&head, &diff, None, DiffScope::All);
assert_eq!(
out.patch,
PatchCoverage {
covered: 1,
uncovered: 1
}
);
assert_eq!(out.patch.percent(), Some(50.0));
assert_eq!(out.uncovered_new_lines, vec![("src/a.rs".to_string(), 3)]);
}
#[test]
fn added_non_executable_lines_excluded_from_denominator() {
let head = report(&[("src/a.rs", &[(1, 1), (2, 0)])]);
let diff = diff_added("src/a.rs", false, &[2, 5]);
let out = analyze(&head, &diff, None, DiffScope::All);
assert_eq!(
out.patch,
PatchCoverage {
covered: 0,
uncovered: 1
}
);
}
#[test]
fn new_file_patch_coverage() {
let head = report(&[("src/new.rs", &[(1, 1), (2, 0), (3, 1)])]);
let diff = diff_added("src/new.rs", true, &[1, 2, 3]);
let out = analyze(&head, &diff, None, DiffScope::All);
assert_eq!(
out.patch,
PatchCoverage {
covered: 2,
uncovered: 1
}
);
assert_eq!(out.file_patches.len(), 1);
assert_eq!(out.file_patches[0].uncovered_lines, vec![2]);
}
#[test]
fn project_delta_with_baseline() {
let baseline = report(&[("src/a.rs", &[(1, 1), (2, 0)])]); let head = report(&[("src/a.rs", &[(1, 1), (2, 1)])]); let diff = diff_added("src/a.rs", false, &[2]);
let out = analyze(&head, &diff, Some(&baseline), DiffScope::All);
assert!(out.has_baseline);
assert_eq!(out.total_before, Some(50.0));
assert_eq!(out.total_after, Some(100.0));
assert_eq!(out.file_deltas.len(), 1);
assert_eq!(out.file_deltas[0].delta(), Some(50.0));
}
#[test]
fn delta_for_new_file_is_after_minus_nothing() {
let baseline = report(&[]);
let head = report(&[("src/new.rs", &[(1, 1)])]);
let diff = diff_added("src/new.rs", true, &[1]);
let out = analyze(&head, &diff, Some(&baseline), DiffScope::All);
assert_eq!(out.file_deltas[0].before, None);
assert_eq!(out.file_deltas[0].after, Some(100.0));
}
#[test]
fn indirect_change_on_unchanged_file() {
let baseline = report(&[("src/b.rs", &[(5, 3)])]);
let head = report(&[("src/b.rs", &[(5, 0)])]);
let diff = diff_added("src/a.rs", true, &[1]); let out = analyze(&head, &diff, Some(&baseline), DiffScope::All);
assert_eq!(out.indirect.len(), 1);
assert_eq!(out.indirect[0].path, "src/b.rs");
assert_eq!(out.indirect[0].base_line, 5);
assert!(!out.indirect[0].became_covered);
assert_eq!(out.indirect_newly_uncovered(), 1);
}
#[test]
fn patch_percent_none_when_empty() {
assert_eq!(PatchCoverage::default().percent(), None);
assert_eq!(PatchCoverage::default().total(), 0);
}
#[test]
fn file_delta_handles_all_combinations() {
let d = |before, after| FileDelta::new("x", before, after);
assert_eq!(d(Some(80.0), Some(90.0)).delta(), Some(10.0));
assert_eq!(d(Some(50.0), None).delta(), Some(-50.0));
assert_eq!(d(None, Some(50.0)).delta(), None);
}
#[test]
fn indirect_change_newly_covered() {
let baseline = report(&[("src/b.rs", &[(5, 0)])]);
let head = report(&[("src/b.rs", &[(5, 3)])]);
let diff = diff_added("src/a.rs", true, &[1]);
let out = analyze(&head, &diff, Some(&baseline), DiffScope::All);
assert_eq!(out.indirect_newly_covered(), 1);
assert!(out.indirect[0].became_covered);
}
#[test]
fn added_lines_are_not_counted_as_indirect() {
let baseline = report(&[("src/a.rs", &[(1, 1)])]);
let head = report(&[("src/a.rs", &[(1, 0)])]);
let diff = diff_added("src/a.rs", true, &[1]); let out = analyze(&head, &diff, Some(&baseline), DiffScope::All);
assert!(out.indirect.is_empty());
}
#[test]
fn diff_only_suppresses_untouched_file_indirect() {
let baseline = report(&[("src/b.rs", &[(5, 3)])]);
let head = report(&[("src/b.rs", &[(5, 0)])]);
let diff = diff_added("src/a.rs", true, &[1]); let out = analyze(&head, &diff, Some(&baseline), DiffScope::DiffOnly);
assert!(
out.indirect.is_empty(),
"an untouched-file flip is cross-run noise under DiffOnly"
);
assert!(out.notable_unchanged.is_empty());
}
#[test]
fn diff_only_delta_table_scoped_to_changed_files() {
let baseline = report(&[
("src/a.rs", &[(1, 1), (2, 0)]),
("src/b.rs", &[(1, 1), (2, 1)]),
]);
let head = report(&[
("src/a.rs", &[(1, 1), (2, 1)]),
("src/b.rs", &[(1, 1), (2, 0)]),
]);
let diff = diff_added("src/a.rs", false, &[2]); let out = analyze(&head, &diff, Some(&baseline), DiffScope::DiffOnly);
let paths: Vec<&str> = out.file_deltas.iter().map(|d| d.path.as_str()).collect();
assert_eq!(paths, vec!["src/a.rs"], "only the changed file appears");
assert!(out.notable_unchanged.is_empty(), "b.rs moved < threshold");
}
#[test]
fn diff_only_surfaces_substantial_unchanged_move() {
let before: Vec<(u32, u64)> = (1..=12).map(|n| (n, 1)).collect();
let after: Vec<(u32, u64)> = (1..=12).map(|n| (n, 0)).collect();
let baseline = report(&[("src/c.rs", &before)]);
let head = report(&[("src/c.rs", &after)]);
let diff = diff_added("src/a.rs", true, &[1]); let out = analyze(&head, &diff, Some(&baseline), DiffScope::DiffOnly);
assert!(out.file_deltas.is_empty(), "c.rs is not in the diff");
assert_eq!(
out.notable_unchanged.len(),
1,
"12-line drop exceeds threshold"
);
assert_eq!(out.notable_unchanged[0].path, "src/c.rs");
assert!(
out.indirect.is_empty(),
"per-line indirect still suppressed"
);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod marker_tests {
use super::tests::*;
use super::*;
use crate::coverage::markers::Region;
fn tolerate(path: &str, lines: &[u32]) -> Markers {
let regions = lines
.iter()
.map(|&line| Region {
kind: MarkerKind::Tolerate,
start: line,
end: line,
reason: "CPU-gated".to_string(),
})
.collect();
Markers {
head: BTreeMap::from([(path.to_string(), FileMarkers::new(regions))]),
base: BTreeMap::new(),
}
}
#[test]
fn tolerated_flip_in_an_untouched_file_does_not_move_the_headline() {
let head = report(&[
("src/gated.rs", &[(1, 0), (2, 0)]),
("src/other.rs", &[(1, 1), (2, 1)]),
]);
let baseline = report(&[
("src/gated.rs", &[(1, 5), (2, 5)]),
("src/other.rs", &[(1, 1), (2, 1)]),
]);
let diff = DiffModel::default();
let bare = analyze(&head, &diff, Some(&baseline), DiffScope::DiffOnly);
assert_eq!(bare.total_after, Some(50.0));
assert_eq!(bare.total_after_effective, Some(50.0));
assert_eq!(bare.total_before, Some(100.0));
let markers = tolerate("src/gated.rs", &[1, 2]);
let masked =
analyze_with_markers(&head, &diff, Some(&baseline), DiffScope::DiffOnly, &markers);
assert_eq!(
masked.total_after,
Some(50.0),
"the reported percentage must stay the real measured value"
);
assert_eq!(
masked.total_after_effective,
Some(100.0),
"the headline delta must see the baseline status of tolerated lines"
);
}
#[test]
fn untolerated_lines_in_a_tolerated_file_still_count() {
let head = report(&[("src/gated.rs", &[(1, 0), (2, 0)])]);
let baseline = report(&[("src/gated.rs", &[(1, 5), (2, 5)])]);
let markers = tolerate("src/gated.rs", &[1]);
let out = analyze_with_markers(
&head,
&DiffModel::default(),
Some(&baseline),
DiffScope::DiffOnly,
&markers,
);
assert_eq!(out.total_after, Some(0.0));
assert_eq!(out.total_after_effective, Some(50.0));
}
#[test]
fn a_tolerated_added_line_keeps_its_real_status() {
let head = report(&[("src/a.rs", &[(1, 1), (2, 0)])]);
let baseline = report(&[("src/a.rs", &[(1, 1)])]);
let diff = diff_added("src/a.rs", false, &[2]);
let markers = tolerate("src/a.rs", &[2]);
let out =
analyze_with_markers(&head, &diff, Some(&baseline), DiffScope::DiffOnly, &markers);
assert_eq!(
out.total_after_effective,
Some(50.0),
"an added line has no baseline status to inherit"
);
assert_eq!(out.patch.covered, 0);
assert_eq!(
out.patch.uncovered, 1,
"a tolerated added line stays in the patch denominator"
);
}
#[test]
fn per_file_delta_is_masked_but_the_percentage_is_real() {
let head = report(&[("src/a.rs", &[(1, 0), (2, 1)])]);
let baseline = report(&[("src/a.rs", &[(1, 5), (2, 1)])]);
let diff = diff_added("src/a.rs", false, &[]);
let markers = tolerate("src/a.rs", &[1]);
let out =
analyze_with_markers(&head, &diff, Some(&baseline), DiffScope::DiffOnly, &markers);
let fd = &out.file_deltas[0];
assert_eq!(fd.after, Some(50.0), "displayed percentage stays real");
assert_eq!(fd.after_effective, Some(100.0));
assert_eq!(fd.delta(), Some(0.0));
assert!(fd.is_masked());
}
#[test]
fn tolerated_flip_does_not_reach_the_notable_threshold() {
let lines_head: Vec<(u32, u64)> = (1..=12).map(|n| (n, 0)).collect();
let lines_base: Vec<(u32, u64)> = (1..=12).map(|n| (n, 3)).collect();
let head = report(&[("src/gated.rs", &lines_head)]);
let baseline = report(&[("src/gated.rs", &lines_base)]);
let diff = DiffModel::default();
let bare = analyze(&head, &diff, Some(&baseline), DiffScope::DiffOnly);
assert_eq!(bare.notable_unchanged.len(), 1, "12 lines flipped");
let all: Vec<u32> = (1..=12).collect();
let markers = tolerate("src/gated.rs", &all);
let masked =
analyze_with_markers(&head, &diff, Some(&baseline), DiffScope::DiffOnly, &markers);
assert!(masked.notable_unchanged.is_empty());
}
#[test]
fn indirect_changes_skip_tolerated_lines() {
let head = report(&[("src/a.rs", &[(1, 0), (2, 0)])]);
let baseline = report(&[("src/a.rs", &[(1, 5), (2, 5)])]);
let diff = diff_added("src/a.rs", false, &[]);
let bare = analyze(&head, &diff, Some(&baseline), DiffScope::DiffOnly);
assert_eq!(bare.indirect.len(), 2);
let markers = tolerate("src/a.rs", &[1]);
let masked =
analyze_with_markers(&head, &diff, Some(&baseline), DiffScope::DiffOnly, &markers);
assert_eq!(masked.indirect.len(), 1);
assert_eq!(masked.indirect[0].head_line, 2);
}
#[test]
fn tolerate_is_inert_without_a_baseline() {
let head = report(&[("src/a.rs", &[(1, 0), (2, 1)])]);
let markers = tolerate("src/a.rs", &[1]);
let out = analyze_with_markers(
&head,
&diff_added("src/a.rs", false, &[]),
None,
DiffScope::DiffOnly,
&markers,
);
assert_eq!(out.total_after, Some(50.0));
assert_eq!(out.total_after_effective, None);
}
#[test]
fn applied_markers_collapse_when_identical_on_both_sides() {
let shared = Region {
kind: MarkerKind::Tolerate,
start: 3,
end: 5,
reason: "CPU-gated".to_string(),
};
let base_only = Region {
kind: MarkerKind::Ignore,
start: 9,
end: 9,
reason: "removed in head".to_string(),
};
let markers = Markers {
head: BTreeMap::from([(
"src/a.rs".to_string(),
FileMarkers::new(vec![shared.clone()]),
)]),
base: BTreeMap::from([(
"src/a.rs".to_string(),
FileMarkers::new(vec![shared, base_only]),
)]),
};
let out = analyze_with_markers(
&report(&[("src/a.rs", &[(1, 1)])]),
&DiffModel::default(),
None,
DiffScope::DiffOnly,
&markers,
);
assert_eq!(out.markers.len(), 2);
assert_eq!(out.markers[0].side, MarkerSide::Both);
assert_eq!(out.markers[0].start, 3);
assert_eq!(out.markers[1].side, MarkerSide::Base);
assert_eq!(out.markers[1].kind, MarkerKind::Ignore);
}
}