use crate::cli::IncrementalCoverageOutputFormat;
use crate::services::facades::incremental_coverage_facade::{
IncrementalCoverageFacade, IncrementalCoverageRequest, IncrementalCoverageResult,
};
use crate::services::service_registry::ServiceRegistry;
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct IncrementalCoverageConfig {
pub project_path: PathBuf,
pub base_branch: String,
pub target_branch: Option<String>,
pub format: IncrementalCoverageOutputFormat,
pub coverage_threshold: f64,
pub changed_files_only: bool,
pub detailed: bool,
pub output: Option<PathBuf>,
pub perf: bool,
pub cache_dir: Option<PathBuf>,
pub force_refresh: bool,
pub top_files: usize,
}
pub async fn handle_analyze_incremental_coverage(config: IncrementalCoverageConfig) -> Result<()> {
print_analysis_header(
&config.project_path,
&config.base_branch,
&config.target_branch,
config.coverage_threshold,
);
let registry = Arc::new(ServiceRegistry::new());
let facade = IncrementalCoverageFacade::new(registry);
let request = IncrementalCoverageRequest {
project_path: config.project_path.clone(),
base_branch: config.base_branch.clone(),
target_branch: config.target_branch.clone(),
coverage_threshold: config.coverage_threshold,
changed_files_only: config.changed_files_only,
detailed: config.detailed,
cache_dir: config.cache_dir.clone(),
force_refresh: config.force_refresh,
top_files: config.top_files,
};
let result = facade.analyze_project(request).await?;
output_results(result, config.format, config.output, config.top_files).await?;
eprintln!("✅ Incremental coverage analysis complete");
Ok(())
}
fn print_analysis_header(
project_path: &Path,
base_branch: &str,
target_branch: &Option<String>,
coverage_threshold: f64,
) {
eprintln!("📊 Analyzing incremental coverage...");
eprintln!("📁 Project path: {}", project_path.display());
eprintln!("🌿 Base branch: {base_branch}");
eprintln!(
"🎯 Target branch: {}",
target_branch.as_deref().unwrap_or("HEAD")
);
eprintln!("📈 Coverage threshold: {:.1}%", coverage_threshold * 100.0);
}
async fn output_results(
result: IncrementalCoverageResult,
format: IncrementalCoverageOutputFormat,
output: Option<PathBuf>,
top_files: usize,
) -> Result<()> {
let content = format_result(result, format, top_files)?;
if let Some(output_path) = output {
tokio::fs::write(&output_path, &content).await?;
eprintln!("📝 Written to {}", output_path.display());
} else {
println!("{content}");
}
Ok(())
}
fn format_result(
result: IncrementalCoverageResult,
format: IncrementalCoverageOutputFormat,
top_files: usize,
) -> Result<String> {
match format {
IncrementalCoverageOutputFormat::Summary => Ok(format_summary(&result, top_files)),
IncrementalCoverageOutputFormat::Detailed => Ok(format_detailed(&result, top_files)),
IncrementalCoverageOutputFormat::Json => {
serde_json::to_string_pretty(&result).map_err(Into::into)
}
IncrementalCoverageOutputFormat::Markdown => Ok(format_markdown(&result, top_files)),
IncrementalCoverageOutputFormat::Lcov => Ok(format_lcov(&result)),
IncrementalCoverageOutputFormat::Delta => Ok(format_delta(&result, top_files)),
IncrementalCoverageOutputFormat::Sarif => Ok(format_sarif(&result)),
}
}
fn format_summary(result: &IncrementalCoverageResult, top_files: usize) -> String {
let mut output = String::new();
output.push_str("# Incremental Coverage Summary\n\n");
output.push_str(&result.summary);
output.push_str("\n\n## Top Changed Files\n");
for (i, file) in result.changed_files.iter().take(top_files).enumerate() {
output.push_str(&format!(
"{}. {} - {:.1}% → {:.1}% (Δ{:+.1}%)\n",
i + 1,
file.file_path,
file.coverage_before * 100.0,
file.coverage_after * 100.0,
file.coverage_delta * 100.0
));
}
output
}
fn format_detailed(result: &IncrementalCoverageResult, top_files: usize) -> String {
let mut output = String::new();
output.push_str("# Incremental Coverage Detailed Report\n\n");
output.push_str(&format!("Total files analyzed: {}\n", result.total_files));
output.push_str(&format!("Files with coverage: {}\n", result.covered_files));
output.push_str(&format!(
"Overall coverage: {:.1}%\n",
result.coverage_percentage * 100.0
));
output.push_str(&format!(
"Files above threshold: {}\n",
result.files_above_threshold
));
output.push_str(&format!(
"Files below threshold: {}\n\n",
result.files_below_threshold
));
output.push_str(&format!("## Changed Files (Top {top_files})\n"));
for file in result.changed_files.iter().take(top_files) {
output.push_str(&format!("\n### {}\n", file.file_path));
output.push_str(&format!("- Status: {:?}\n", file.status));
output.push_str(&format!(
"- Coverage: {:.1}% → {:.1}%\n",
file.coverage_before * 100.0,
file.coverage_after * 100.0
));
output.push_str(&format!("- Delta: {:+.1}%\n", file.coverage_delta * 100.0));
output.push_str(&format!(
"- Lines: {}/{}\n",
file.lines_covered, file.lines_total
));
}
output
}
fn format_markdown(result: &IncrementalCoverageResult, top_files: usize) -> String {
let mut output = String::new();
output.push_str("# Incremental Coverage Report\n\n");
output.push_str(&format!("**Summary:** {}\n\n", result.summary));
output.push_str("## Metrics\n\n");
output.push_str("| Metric | Value |\n");
output.push_str("|--------|-------|\n");
output.push_str(&format!("| Total Files | {} |\n", result.total_files));
output.push_str(&format!("| Covered Files | {} |\n", result.covered_files));
output.push_str(&format!(
"| Coverage | {:.1}% |\n",
result.coverage_percentage * 100.0
));
output.push_str(&format!(
"| Above Threshold | {} |\n",
result.files_above_threshold
));
output.push_str(&format!(
"| Below Threshold | {} |\n\n",
result.files_below_threshold
));
output.push_str("## Top Changed Files\n\n");
output.push_str("| File | Before | After | Delta | Status |\n");
output.push_str("|------|--------|-------|-------|--------|\n");
for file in result.changed_files.iter().take(top_files) {
output.push_str(&format!(
"| {} | {:.1}% | {:.1}% | {:+.1}% | {:?} |\n",
file.file_path,
file.coverage_before * 100.0,
file.coverage_after * 100.0,
file.coverage_delta * 100.0,
file.status
));
}
output
}
fn format_lcov(result: &IncrementalCoverageResult) -> String {
let mut output = String::new();
for file in &result.changed_files {
output.push_str(&format!("SF:{}\n", file.file_path));
output.push_str(&format!("DA:{},{}\n", file.lines_total, file.lines_covered));
output.push_str(&format!("LH:{}\n", file.lines_covered));
output.push_str(&format!("LF:{}\n", file.lines_total));
output.push_str("end_of_record\n");
}
output
}
fn format_delta(result: &IncrementalCoverageResult, top_files: usize) -> String {
let mut output = String::new();
output.push_str("Coverage Delta Report\n");
output.push_str("====================\n\n");
let improved: Vec<_> = result
.changed_files
.iter()
.filter(|f| f.coverage_delta > 0.0)
.take(top_files)
.collect();
let degraded: Vec<_> = result
.changed_files
.iter()
.filter(|f| f.coverage_delta < 0.0)
.take(top_files)
.collect();
if !improved.is_empty() {
output.push_str("✅ Improved Coverage:\n");
for file in improved {
output.push_str(&format!(
" {} {:+.1}%\n",
file.file_path,
file.coverage_delta * 100.0
));
}
output.push('\n');
}
if !degraded.is_empty() {
output.push_str("⚠️ Degraded Coverage:\n");
for file in degraded {
output.push_str(&format!(
" {} {:+.1}%\n",
file.file_path,
file.coverage_delta * 100.0
));
}
}
output
}
fn format_sarif(result: &IncrementalCoverageResult) -> String {
serde_json::json!({
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "pmat-incremental-coverage",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit"
}
},
"results": result.changed_files.iter().filter(|f| f.coverage_delta < 0.0).map(|file| {
serde_json::json!({
"ruleId": "coverage-degradation",
"level": "warning",
"message": {
"text": format!(
"Coverage degraded by {:.1}% (from {:.1}% to {:.1}%)",
file.coverage_delta.abs() * 100.0,
file.coverage_before * 100.0,
file.coverage_after * 100.0
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": file.file_path.clone()
}
}
}]
})
}).collect::<Vec<_>>()
}]
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_summary() {
let result = IncrementalCoverageResult {
total_files: 10,
covered_files: 8,
coverage_percentage: 0.8,
files_above_threshold: 6,
files_below_threshold: 4,
changed_files: vec![],
summary: "Test summary".to_string(),
};
let output = format_summary(&result, 5);
assert!(output.contains("Test summary"));
assert!(output.contains("# Incremental Coverage Summary"));
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}