1#[derive(Debug, Clone, Default, serde::Serialize)]
27pub struct CoverageReport {
28 pub covered_offsets: u32,
30 pub bitmap_size: u32,
32 pub bitmap: Vec<u8>,
34}
35
36impl CoverageReport {
37 #[must_use]
39 pub fn coverage_ratio(&self) -> f64 {
40 if self.bitmap_size == 0 {
41 return 0.0;
42 }
43 f64::from(self.covered_offsets) / (f64::from(self.bitmap_size) * 8.0)
44 }
45
46 #[must_use]
49 pub fn new_coverage_since(&self, previous: &CoverageReport) -> u32 {
50 if self.bitmap.len() != previous.bitmap.len() {
51 return self.covered_offsets;
52 }
53 let mut new_bits = 0u32;
54 for (current, prev) in self.bitmap.iter().zip(previous.bitmap.iter()) {
55 let new = current & !prev;
57 new_bits += new.count_ones();
58 }
59 new_bits
60 }
61
62 pub fn merge(&mut self, other: &CoverageReport) {
64 if self.bitmap.len() != other.bitmap.len() {
65 return;
66 }
67 for (a, b) in self.bitmap.iter_mut().zip(other.bitmap.iter()) {
68 *a |= b;
69 }
70 self.covered_offsets = self.bitmap.iter().map(|b| b.count_ones()).sum();
71 }
72}
73
74#[derive(Debug, Clone, Default)]
77pub struct CoverageAccumulator {
78 merged: Vec<u8>,
79 total_runs: u64,
80 total_covered: u32,
81}
82
83impl CoverageAccumulator {
84 #[must_use]
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn add_run(&mut self, report: &CoverageReport) -> u32 {
91 if self.merged.is_empty() {
92 self.merged.clone_from(&report.bitmap);
93 self.total_runs = 1;
94 self.total_covered = report.covered_offsets;
95 return report.covered_offsets;
96 }
97
98 if self.merged.len() != report.bitmap.len() {
99 return 0;
100 }
101
102 let mut new_bits = 0u32;
103 for (acc, run) in self.merged.iter_mut().zip(report.bitmap.iter()) {
104 let new = run & !*acc;
105 new_bits += new.count_ones();
106 *acc |= run;
107 }
108 self.total_runs += 1;
109 self.total_covered = self.merged.iter().map(|b| b.count_ones()).sum();
110 new_bits
111 }
112
113 #[must_use]
115 pub fn total_covered(&self) -> u32 {
116 self.total_covered
117 }
118
119 #[must_use]
121 pub fn total_runs(&self) -> u64 {
122 self.total_runs
123 }
124
125 #[must_use]
127 #[expect(clippy::cast_precision_loss)]
128 pub fn coverage_ratio(&self) -> f64 {
129 if self.merged.is_empty() {
130 return 0.0;
131 }
132 f64::from(self.total_covered) / (self.merged.len() as f64 * 8.0)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn new_coverage_detected() {
142 let prev = CoverageReport {
143 covered_offsets: 2,
144 bitmap_size: 1,
145 bitmap: vec![0b00000011],
146 };
147 let curr = CoverageReport {
148 covered_offsets: 3,
149 bitmap_size: 1,
150 bitmap: vec![0b00000111],
151 };
152 assert_eq!(curr.new_coverage_since(&prev), 1);
153 }
154
155 #[test]
156 fn accumulator_tracks_total() {
157 let mut acc = CoverageAccumulator::new();
158 let r1 = CoverageReport {
159 covered_offsets: 2,
160 bitmap_size: 1,
161 bitmap: vec![0b00000011],
162 };
163 let new1 = acc.add_run(&r1);
164 assert_eq!(new1, 2);
165
166 let r2 = CoverageReport {
167 covered_offsets: 2,
168 bitmap_size: 1,
169 bitmap: vec![0b00001100],
170 };
171 let new2 = acc.add_run(&r2);
172 assert_eq!(new2, 2);
173 assert_eq!(acc.total_covered(), 4);
174 }
175
176 #[test]
177 fn merge_unions_bitmaps() {
178 let mut a = CoverageReport {
179 covered_offsets: 2,
180 bitmap_size: 1,
181 bitmap: vec![0b00000011],
182 };
183 let b = CoverageReport {
184 covered_offsets: 2,
185 bitmap_size: 1,
186 bitmap: vec![0b00001100],
187 };
188 a.merge(&b);
189 assert_eq!(a.covered_offsets, 4);
190 assert_eq!(a.bitmap, vec![0b00001111]);
191 }
192}