use std::collections::BTreeMap;
use crate::binding::{BindingRegistry, ImplStatus};
use crate::schema::Contract;
#[derive(Debug, Clone)]
pub struct CoverageReport {
pub contracts: Vec<ContractCoverage>,
pub totals: CoverageTotals,
}
#[derive(Debug, Clone)]
pub struct ContractCoverage {
pub stem: String,
pub equations: usize,
pub obligations: usize,
pub falsification_covered: usize,
pub kani_covered: usize,
pub binding_implemented: usize,
pub binding_partial: usize,
pub binding_missing: usize,
pub by_type: BTreeMap<String, usize>,
}
#[derive(Debug, Clone)]
pub struct CoverageTotals {
pub contracts: usize,
pub equations: usize,
pub obligations: usize,
pub falsification_tests: usize,
pub kani_harnesses: usize,
pub binding_implemented: usize,
pub binding_partial: usize,
pub binding_missing: usize,
}
pub fn coverage_report(
contracts: &[(String, &Contract)],
binding: Option<&BindingRegistry>,
) -> CoverageReport {
let mut results = Vec::new();
let mut totals = CoverageTotals {
contracts: contracts.len(),
equations: 0,
obligations: 0,
falsification_tests: 0,
kani_harnesses: 0,
binding_implemented: 0,
binding_partial: 0,
binding_missing: 0,
};
for (stem, contract) in contracts {
let contract_file = format!("{stem}.yaml");
let equations = contract.equations.len();
let obligations = contract.proof_obligations.len();
let ft_count = contract.falsification_tests.len();
let kh_count = contract.kani_harnesses.len();
let mut by_type: BTreeMap<String, usize> = BTreeMap::new();
for ob in &contract.proof_obligations {
*by_type.entry(ob.obligation_type.to_string()).or_default() += 1;
}
let falsification_covered = if ft_count > 0 { obligations } else { 0 };
let kani_covered = kh_count.min(obligations);
let (binding_implemented, binding_partial, binding_missing) = if let Some(reg) = binding {
count_binding_coverage(&contract_file, contract, reg)
} else {
(0, 0, equations)
};
totals.equations += equations;
totals.obligations += obligations;
totals.falsification_tests += ft_count;
totals.kani_harnesses += kh_count;
totals.binding_implemented += binding_implemented;
totals.binding_partial += binding_partial;
totals.binding_missing += binding_missing;
results.push(ContractCoverage {
stem: stem.clone(),
equations,
obligations,
falsification_covered,
kani_covered,
binding_implemented,
binding_partial,
binding_missing,
by_type,
});
}
CoverageReport {
contracts: results,
totals,
}
}
fn count_binding_coverage(
contract_file: &str,
contract: &Contract,
binding: &BindingRegistry,
) -> (usize, usize, usize) {
let mut implemented = 0usize;
let mut partial = 0usize;
let mut missing = 0usize;
for eq_name in contract.equations.keys() {
let status = binding
.bindings
.iter()
.find(|b| b.contract == contract_file && b.equation == *eq_name)
.map(|b| b.status);
match status {
Some(ImplStatus::Implemented) => implemented += 1,
Some(ImplStatus::Partial) => partial += 1,
Some(ImplStatus::NotImplemented) | None => missing += 1,
}
}
(implemented, partial, missing)
}
pub fn overall_percentage(report: &CoverageReport) -> f64 {
if report.totals.obligations == 0 {
return 100.0;
}
let covered: usize = report
.contracts
.iter()
.map(|c| c.falsification_covered.max(c.kani_covered))
.sum();
#[allow(clippy::cast_precision_loss)]
let pct = (covered as f64 / report.totals.obligations as f64) * 100.0;
pct
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::parse_contract_str;
fn contract_with_obs(n_obligations: usize, n_ft: usize, n_kani: usize) -> Contract {
let mut yaml = String::from(
r#"
metadata:
version: "1.0.0"
description: "Test"
references: ["Paper"]
equations:
f:
formula: "f(x) = x"
proof_obligations:
"#,
);
for i in 0..n_obligations {
yaml.push_str(&format!(
" - type: invariant\n property: \"prop {i}\"\n"
));
}
yaml.push_str("falsification_tests:\n");
for i in 0..n_ft {
yaml.push_str(&format!(
" - id: FT-{i:03}\n rule: \"r\"\n \
prediction: \"p\"\n if_fails: \"f\"\n"
));
}
yaml.push_str("kani_harnesses:\n");
for i in 0..n_kani {
yaml.push_str(&format!(
" - id: KH-{i:03}\n obligation: OBL-{i:03}\n \
bound: 16\n"
));
}
parse_contract_str(&yaml).unwrap()
}
#[test]
fn empty_contracts() {
let report = coverage_report(&[], None);
assert_eq!(report.totals.contracts, 0);
assert_eq!(overall_percentage(&report), 100.0);
}
#[test]
fn single_contract_full_coverage() {
let c = contract_with_obs(3, 3, 3);
let report = coverage_report(&[("test".to_string(), &c)], None);
assert_eq!(report.totals.obligations, 3);
assert_eq!(report.totals.falsification_tests, 3);
assert_eq!(report.totals.kani_harnesses, 3);
assert_eq!(report.contracts[0].falsification_covered, 3);
assert_eq!(report.contracts[0].kani_covered, 3);
assert!((overall_percentage(&report) - 100.0).abs() < 0.01);
}
#[test]
fn no_tests_zero_coverage() {
let c = contract_with_obs(5, 0, 0);
let report = coverage_report(&[("test".to_string(), &c)], None);
assert_eq!(report.contracts[0].falsification_covered, 0);
assert_eq!(report.contracts[0].kani_covered, 0);
assert!((overall_percentage(&report) - 0.0).abs() < 0.01);
}
#[test]
fn multiple_contracts() {
let c1 = contract_with_obs(2, 2, 1);
let c2 = contract_with_obs(3, 0, 3);
let report = coverage_report(&[("a".to_string(), &c1), ("b".to_string(), &c2)], None);
assert_eq!(report.totals.contracts, 2);
assert_eq!(report.totals.obligations, 5);
assert_eq!(report.totals.equations, 2);
}
#[test]
fn binding_coverage_implemented() {
let c = contract_with_obs(2, 2, 2);
let binding = crate::binding::parse_binding_str(
r#"
version: "1.0.0"
target_crate: test
bindings:
- contract: test.yaml
equation: f
module_path: "test::f"
function: f
status: implemented
"#,
)
.unwrap();
let report = coverage_report(&[("test".to_string(), &c)], Some(&binding));
assert_eq!(report.contracts[0].binding_implemented, 1);
assert_eq!(report.contracts[0].binding_missing, 0);
assert_eq!(report.totals.binding_implemented, 1);
}
#[test]
fn binding_coverage_partial() {
let c = contract_with_obs(1, 1, 1);
let binding = crate::binding::parse_binding_str(
r#"
version: "1.0.0"
target_crate: test
bindings:
- contract: test.yaml
equation: f
module_path: "test::f"
function: f
status: partial
"#,
)
.unwrap();
let report = coverage_report(&[("test".to_string(), &c)], Some(&binding));
assert_eq!(report.contracts[0].binding_partial, 1);
assert_eq!(report.totals.binding_partial, 1);
}
#[test]
fn binding_missing_when_no_entry() {
let c = contract_with_obs(1, 1, 1);
let binding = crate::binding::parse_binding_str(
r#"
version: "1.0.0"
target_crate: test
bindings: []
"#,
)
.unwrap();
let report = coverage_report(&[("test".to_string(), &c)], Some(&binding));
assert_eq!(report.contracts[0].binding_missing, 1);
}
#[test]
fn obligation_type_breakdown() {
let yaml = r#"
metadata:
version: "1.0.0"
description: "Test"
references: ["Paper"]
equations:
f:
formula: "f(x) = x"
proof_obligations:
- type: invariant
property: "p1"
- type: bound
property: "p2"
- type: invariant
property: "p3"
falsification_tests: []
"#;
let c = parse_contract_str(yaml).unwrap();
let report = coverage_report(&[("test".to_string(), &c)], None);
let by_type = &report.contracts[0].by_type;
assert_eq!(by_type.get("invariant"), Some(&2));
assert_eq!(by_type.get("bound"), Some(&1));
}
#[test]
fn kani_covered_capped_at_obligations() {
let c = contract_with_obs(2, 0, 10);
let report = coverage_report(&[("test".to_string(), &c)], None);
assert_eq!(report.contracts[0].kani_covered, 2);
}
#[test]
fn no_binding_defaults_to_missing() {
let c = contract_with_obs(1, 1, 1);
let report = coverage_report(&[("test".to_string(), &c)], None);
assert_eq!(report.contracts[0].binding_missing, 1);
}
}