use std::collections::HashMap;
use crate::spec::op_registry;
use crate::spec::registry::error::CoverageError;
use crate::spec::registry::verify::verify_specs;
use vyre_spec::Category;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoverageReport {
pub passed: bool,
pub errors: Vec<CoverageError>,
pub total_ops: usize,
pub ops_per_category: HashMap<String, usize>,
pub total_laws: usize,
}
#[inline]
pub fn spec_coverage_check() -> CoverageReport {
let specs = op_registry::all_specs();
coverage_report_for_specs(&specs)
}
#[inline]
pub fn coverage_report_for_specs(specs: &[crate::OpSpec]) -> CoverageReport {
let mut errors = match verify_specs(specs) {
Ok(()) => Vec::new(),
Err(e) => e,
};
errors.extend(mandatory_field_errors(specs));
let mut ops_per_category = HashMap::new();
let mut total_laws = 0;
for spec in specs {
let cat_key = category_label(&spec.category);
*ops_per_category.entry(cat_key).or_insert(0) += 1;
total_laws += spec.laws.len();
}
CoverageReport {
passed: errors.is_empty(),
errors,
total_ops: specs.len(),
ops_per_category,
total_laws,
}
}
fn mandatory_field_errors(specs: &[crate::OpSpec]) -> Vec<CoverageError> {
let mut errors = Vec::new();
for spec in specs {
if matches!(&spec.category, Category::A { composition_of } if composition_of.is_empty()) {
errors.push(CoverageError::UnclassifiedCategory {
op_id: spec.id.to_string(),
});
}
if spec.archetypes.is_empty() {
errors.push(CoverageError::InvalidOracle {
op_id: spec.id.to_string(),
message: "missing archetypes. Fix: declare at least one archetype id for generator coverage.".to_string(),
});
}
if spec.mutation_sensitivity.is_empty() {
errors.push(CoverageError::InvalidOracle {
op_id: spec.id.to_string(),
message: "missing mutation_sensitivity. Fix: declare the mutation classes this op's tests must kill.".to_string(),
});
}
if spec.spec_table.is_empty() {
errors.push(CoverageError::InvalidOracle {
op_id: spec.id.to_string(),
message: "missing spec_table. Fix: add at least one load-bearing input/output row."
.to_string(),
});
}
}
errors
}
fn category_label(category: &Category) -> String {
match category {
Category::A { .. } => "A".to_string(),
Category::C { .. } => "C".to_string(),
_ => "unknown".to_string(),
}
}