1use std::collections::BTreeMap;
8
9use crate::binding::{BindingRegistry, ImplStatus};
10use crate::schema::Contract;
11
12#[derive(Debug, Clone)]
14pub struct CoverageReport {
15 pub contracts: Vec<ContractCoverage>,
17 pub totals: CoverageTotals,
19}
20
21#[derive(Debug, Clone)]
23pub struct ContractCoverage {
24 pub stem: String,
26 pub equations: usize,
28 pub obligations: usize,
30 pub falsification_covered: usize,
32 pub kani_covered: usize,
34 pub binding_implemented: usize,
36 pub binding_partial: usize,
38 pub binding_missing: usize,
40 pub by_type: BTreeMap<String, usize>,
42}
43
44#[derive(Debug, Clone)]
46pub struct CoverageTotals {
47 pub contracts: usize,
49 pub equations: usize,
51 pub obligations: usize,
53 pub falsification_tests: usize,
55 pub kani_harnesses: usize,
57 pub binding_implemented: usize,
59 pub binding_partial: usize,
61 pub binding_missing: usize,
63}
64
65pub fn coverage_report(
70 contracts: &[(String, &Contract)],
71 binding: Option<&BindingRegistry>,
72) -> CoverageReport {
73 let mut results = Vec::new();
74 let mut totals = CoverageTotals {
75 contracts: contracts.len(),
76 equations: 0,
77 obligations: 0,
78 falsification_tests: 0,
79 kani_harnesses: 0,
80 binding_implemented: 0,
81 binding_partial: 0,
82 binding_missing: 0,
83 };
84
85 for (stem, contract) in contracts {
86 let contract_file = format!("{stem}.yaml");
87
88 let equations = contract.equations.len();
89 let obligations = contract.proof_obligations.len();
90 let ft_count = contract.falsification_tests.len();
91 let kh_count = contract.kani_harnesses.len();
92
93 let mut by_type: BTreeMap<String, usize> = BTreeMap::new();
95 for ob in &contract.proof_obligations {
96 *by_type.entry(ob.obligation_type.to_string()).or_default() += 1;
97 }
98
99 let falsification_covered = if ft_count > 0 { obligations } else { 0 };
103
104 let kani_covered = kh_count.min(obligations);
106
107 let (binding_implemented, binding_partial, binding_missing) = if let Some(reg) = binding {
109 count_binding_coverage(&contract_file, contract, reg)
110 } else {
111 (0, 0, equations)
112 };
113
114 totals.equations += equations;
115 totals.obligations += obligations;
116 totals.falsification_tests += ft_count;
117 totals.kani_harnesses += kh_count;
118 totals.binding_implemented += binding_implemented;
119 totals.binding_partial += binding_partial;
120 totals.binding_missing += binding_missing;
121
122 results.push(ContractCoverage {
123 stem: stem.clone(),
124 equations,
125 obligations,
126 falsification_covered,
127 kani_covered,
128 binding_implemented,
129 binding_partial,
130 binding_missing,
131 by_type,
132 });
133 }
134
135 CoverageReport {
136 contracts: results,
137 totals,
138 }
139}
140
141fn count_binding_coverage(
143 contract_file: &str,
144 contract: &Contract,
145 binding: &BindingRegistry,
146) -> (usize, usize, usize) {
147 let mut implemented = 0usize;
148 let mut partial = 0usize;
149 let mut missing = 0usize;
150
151 for eq_name in contract.equations.keys() {
152 let status = binding
153 .find_binding(contract_file, eq_name)
154 .map(|b| b.status);
155
156 match status {
157 Some(ImplStatus::Implemented) => implemented += 1,
158 Some(ImplStatus::Partial) => partial += 1,
159 Some(ImplStatus::NotImplemented) | None => missing += 1,
160 Some(ImplStatus::Pending) => {} }
162 }
163
164 (implemented, partial, missing)
165}
166
167pub fn overall_percentage(report: &CoverageReport) -> f64 {
172 if report.totals.obligations == 0 {
173 return 100.0;
174 }
175 let covered: usize = report
176 .contracts
177 .iter()
178 .map(|c| c.falsification_covered.max(c.kani_covered))
179 .sum();
180 #[allow(clippy::cast_precision_loss)]
181 let pct = (covered as f64 / report.totals.obligations as f64) * 100.0;
182 pct
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::schema::parse_contract_str;
189
190 fn contract_with_obs(n_obligations: usize, n_ft: usize, n_kani: usize) -> Contract {
191 let mut yaml = String::from(
192 r#"
193metadata:
194 version: "1.0.0"
195 description: "Test"
196 references: ["Paper"]
197equations:
198 f:
199 formula: "f(x) = x"
200proof_obligations:
201"#,
202 );
203 for i in 0..n_obligations {
204 yaml.push_str(&format!(
205 " - type: invariant\n property: \"prop {i}\"\n"
206 ));
207 }
208 yaml.push_str("falsification_tests:\n");
209 for i in 0..n_ft {
210 yaml.push_str(&format!(
211 " - id: FT-{i:03}\n rule: \"r\"\n \
212 prediction: \"p\"\n if_fails: \"f\"\n"
213 ));
214 }
215 yaml.push_str("kani_harnesses:\n");
216 for i in 0..n_kani {
217 yaml.push_str(&format!(
218 " - id: KH-{i:03}\n obligation: OBL-{i:03}\n \
219 bound: 16\n"
220 ));
221 }
222 parse_contract_str(&yaml).unwrap()
223 }
224
225 #[test]
226 fn empty_contracts() {
227 let report = coverage_report(&[], None);
228 assert_eq!(report.totals.contracts, 0);
229 assert!((overall_percentage(&report) - 100.0).abs() < f64::EPSILON);
230 }
231
232 #[test]
233 fn single_contract_full_coverage() {
234 let c = contract_with_obs(3, 3, 3);
235 let report = coverage_report(&[("test".to_string(), &c)], None);
236 assert_eq!(report.totals.obligations, 3);
237 assert_eq!(report.totals.falsification_tests, 3);
238 assert_eq!(report.totals.kani_harnesses, 3);
239 assert_eq!(report.contracts[0].falsification_covered, 3);
240 assert_eq!(report.contracts[0].kani_covered, 3);
241 assert!((overall_percentage(&report) - 100.0).abs() < 0.01);
242 }
243
244 #[test]
245 fn no_tests_zero_coverage() {
246 let c = contract_with_obs(5, 0, 0);
247 let report = coverage_report(&[("test".to_string(), &c)], None);
248 assert_eq!(report.contracts[0].falsification_covered, 0);
249 assert_eq!(report.contracts[0].kani_covered, 0);
250 assert!((overall_percentage(&report) - 0.0).abs() < 0.01);
251 }
252
253 #[test]
254 fn multiple_contracts() {
255 let c1 = contract_with_obs(2, 2, 1);
256 let c2 = contract_with_obs(3, 0, 3);
257 let report = coverage_report(&[("a".to_string(), &c1), ("b".to_string(), &c2)], None);
258 assert_eq!(report.totals.contracts, 2);
259 assert_eq!(report.totals.obligations, 5);
260 assert_eq!(report.totals.equations, 2);
261 }
262
263 #[test]
264 fn binding_coverage_implemented() {
265 let c = contract_with_obs(2, 2, 2);
266 let binding = crate::binding::parse_binding_str(
267 r#"
268version: "1.0.0"
269target_crate: test
270bindings:
271 - contract: test.yaml
272 equation: f
273 module_path: "test::f"
274 function: f
275 status: implemented
276"#,
277 )
278 .unwrap();
279 let report = coverage_report(&[("test".to_string(), &c)], Some(&binding));
280 assert_eq!(report.contracts[0].binding_implemented, 1);
281 assert_eq!(report.contracts[0].binding_missing, 0);
282 assert_eq!(report.totals.binding_implemented, 1);
283 }
284
285 #[test]
286 fn binding_coverage_partial() {
287 let c = contract_with_obs(1, 1, 1);
288 let binding = crate::binding::parse_binding_str(
289 r#"
290version: "1.0.0"
291target_crate: test
292bindings:
293 - contract: test.yaml
294 equation: f
295 module_path: "test::f"
296 function: f
297 status: partial
298"#,
299 )
300 .unwrap();
301 let report = coverage_report(&[("test".to_string(), &c)], Some(&binding));
302 assert_eq!(report.contracts[0].binding_partial, 1);
303 assert_eq!(report.totals.binding_partial, 1);
304 }
305
306 #[test]
307 fn binding_missing_when_no_entry() {
308 let c = contract_with_obs(1, 1, 1);
309 let binding = crate::binding::parse_binding_str(
310 r#"
311version: "1.0.0"
312target_crate: test
313bindings: []
314"#,
315 )
316 .unwrap();
317 let report = coverage_report(&[("test".to_string(), &c)], Some(&binding));
318 assert_eq!(report.contracts[0].binding_missing, 1);
319 }
320
321 #[test]
322 fn obligation_type_breakdown() {
323 let yaml = r#"
324metadata:
325 version: "1.0.0"
326 description: "Test"
327 references: ["Paper"]
328equations:
329 f:
330 formula: "f(x) = x"
331proof_obligations:
332 - type: invariant
333 property: "p1"
334 - type: bound
335 property: "p2"
336 - type: invariant
337 property: "p3"
338falsification_tests: []
339"#;
340 let c = parse_contract_str(yaml).unwrap();
341 let report = coverage_report(&[("test".to_string(), &c)], None);
342 let by_type = &report.contracts[0].by_type;
343 assert_eq!(by_type.get("invariant"), Some(&2));
344 assert_eq!(by_type.get("bound"), Some(&1));
345 }
346
347 #[test]
348 fn kani_covered_capped_at_obligations() {
349 let c = contract_with_obs(2, 0, 10);
351 let report = coverage_report(&[("test".to_string(), &c)], None);
352 assert_eq!(report.contracts[0].kani_covered, 2);
353 }
354
355 #[test]
356 fn no_binding_defaults_to_missing() {
357 let c = contract_with_obs(1, 1, 1);
358 let report = coverage_report(&[("test".to_string(), &c)], None);
359 assert_eq!(report.contracts[0].binding_missing, 1);
360 }
361}