#[derive(Clone)]
pub struct IncrementalCoverageFacade {
registry: Arc<ServiceRegistry>,
}
impl IncrementalCoverageFacade {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new(registry: Arc<ServiceRegistry>) -> Self {
Self { registry }
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn analyze_project(
&self,
request: IncrementalCoverageRequest,
) -> Result<IncrementalCoverageResult> {
let changed_files = self
.get_changed_files(
&request.project_path,
&request.base_branch,
request.target_branch.as_deref(),
)
.await?;
let coverage_data = self
.analyze_coverage_changes(&request.project_path, &changed_files, &request)
.await?;
Ok(self.build_coverage_result(coverage_data, changed_files, &request))
}
async fn get_changed_files(
&self,
project_path: &Path,
base_branch: &str,
target_branch: Option<&str>,
) -> Result<Vec<(PathBuf, String)>> {
use crate::cli::coverage_helpers::get_changed_files_for_coverage;
get_changed_files_for_coverage(project_path, base_branch, target_branch).await
}
async fn analyze_coverage_changes(
&self,
project_path: &Path,
changed_files: &[(PathBuf, String)],
request: &IncrementalCoverageRequest,
) -> Result<Vec<ChangedFileCoverage>> {
let measured = Self::load_line_coverage(project_path);
let mut coverage_data = Vec::new();
for (path, status) in changed_files {
if status != "M" && status != "A" {
continue;
}
coverage_data.push(Self::file_coverage(path, measured.as_ref()));
if coverage_data.len() >= request.top_files {
break;
}
}
Ok(coverage_data)
}
fn load_line_coverage(
project_path: &Path,
) -> Option<std::collections::HashMap<String, std::collections::HashMap<usize, u64>>> {
crate::services::agent_context::query::discover_line_coverage(project_path)
}
fn file_coverage(
path: &Path,
measured: Option<
&std::collections::HashMap<String, std::collections::HashMap<usize, u64>>,
>,
) -> ChangedFileCoverage {
let key = path.to_string_lossy().to_string();
let lines = measured.and_then(|m| m.get(&key));
let (coverage_after, lines_covered, lines_total) = match lines {
Some(hits) if !hits.is_empty() => {
let total = hits.len();
let covered = hits.values().filter(|count| **count > 0).count();
#[allow(clippy::cast_precision_loss)]
let pct = (covered as f64 / total as f64) * 100.0;
(Some(pct), covered, total)
}
_ => (None, 0, 0),
};
ChangedFileCoverage {
file_path: key,
coverage_before: None,
coverage_after,
coverage_delta: None,
status: CoverageStatus::NotMeasured,
lines_covered,
lines_total,
}
}
fn build_coverage_result(
&self,
coverage_data: Vec<ChangedFileCoverage>,
changed_files: Vec<(PathBuf, String)>,
request: &IncrementalCoverageRequest,
) -> IncrementalCoverageResult {
let total_files = changed_files.len();
let measured: Vec<f64> = coverage_data
.iter()
.filter_map(|f| f.coverage_after)
.collect();
let covered_files = measured.iter().filter(|pct| **pct > 0.0).count();
let files_not_measured = coverage_data.len() - measured.len();
#[allow(clippy::cast_precision_loss)]
let coverage_percentage = if measured.is_empty() {
None
} else {
Some(measured.iter().sum::<f64>() / measured.len() as f64)
};
let files_above_threshold = measured
.iter()
.filter(|pct| **pct >= request.coverage_threshold)
.count();
let files_below_threshold = measured.len() - files_above_threshold;
let coverage_text = coverage_percentage
.map_or_else(|| "not measured".to_string(), |pct| format!("{pct:.1}%"));
let summary = format!(
"Analyzed {} changed files: {} covered (mean {}), {} above threshold ({:.1}%), \
{} below threshold, {} not measured",
total_files,
covered_files,
coverage_text,
files_above_threshold,
request.coverage_threshold,
files_below_threshold,
files_not_measured
);
IncrementalCoverageResult {
total_files,
covered_files,
coverage_percentage,
files_above_threshold,
files_below_threshold,
files_not_measured,
changed_files: coverage_data,
summary,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn quick_analysis(
&self,
project_path: PathBuf,
base_branch: String,
) -> Result<IncrementalCoverageResult> {
let request = IncrementalCoverageRequest {
project_path,
base_branch,
target_branch: None,
coverage_threshold: 80.0,
changed_files_only: true,
detailed: false,
cache_dir: None,
force_refresh: false,
top_files: 10,
};
self.analyze_project(request).await
}
}