impl CoverageImprovementService {
pub async fn improve_coverage(&self) -> Result<CoverageImprovementReport> {
let baseline = self.measure_baseline_coverage().await?;
if baseline >= self.config.target_coverage {
return Ok(CoverageImprovementReport {
baseline_coverage: baseline,
target_coverage: self.config.target_coverage,
final_coverage: baseline,
iterations: vec![],
success: true,
stop_reason: "Already at target coverage".to_string(),
});
}
let mut current_coverage = baseline;
let mut iterations = Vec::new();
for iteration in 1..=self.config.max_iterations {
if current_coverage >= self.config.target_coverage {
return Ok(CoverageImprovementReport {
baseline_coverage: baseline,
target_coverage: self.config.target_coverage,
final_coverage: current_coverage,
iterations,
success: true,
stop_reason: format!("Target coverage reached in {} iterations", iteration - 1),
});
}
let iteration_report = self.run_iteration(iteration, current_coverage).await?;
current_coverage = baseline
+ iterations
.iter()
.map(|i: &IterationReport| i.coverage_gain)
.sum::<f64>()
+ iteration_report.coverage_gain;
iterations.push(iteration_report);
}
Ok(CoverageImprovementReport {
baseline_coverage: baseline,
target_coverage: self.config.target_coverage,
final_coverage: current_coverage,
iterations,
success: current_coverage >= self.config.target_coverage,
stop_reason: format!("Max iterations ({}) reached", self.config.max_iterations),
})
}
async fn measure_baseline_coverage(&self) -> Result<f64> {
eprintln!("đ Running coverage analysis...");
let makefile_dir = self.find_makefile_directory()?;
eprintln!(" đ Running from: {}", makefile_dir.display());
let output = Command::new("make")
.arg("coverage")
.current_dir(&makefile_dir)
.output()
.await
.context("Failed to execute `make coverage`")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"make coverage failed with exit code {:?}\nstderr: {}",
output.status.code(),
stderr
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
Self::parse_coverage_percentage(&stdout)
.context("Failed to parse coverage from make coverage output")
}
fn find_makefile_directory(&self) -> Result<PathBuf> {
let mut current = self.config.project_path.clone();
if current.is_relative() {
current = std::env::current_dir()?.join(¤t);
}
current = current.canonicalize().unwrap_or(current);
for _ in 0..5 {
let makefile = current.join("Makefile");
if makefile.exists() {
return Ok(current);
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
anyhow::bail!(
"Could not find Makefile in {} or parent directories",
self.config.project_path.display()
)
}
pub(crate) fn parse_coverage_percentage(output: &str) -> Result<f64> {
for line in output.lines() {
if line.trim().starts_with("TOTAL") {
let parts: Vec<&str> = line.split_whitespace().collect();
let percentages: Vec<&str> =
parts.iter().filter(|s| s.contains('%')).copied().collect();
if let Some(last_pct) = percentages.last() {
let pct_str = last_pct.trim_end_matches('%');
let coverage = pct_str
.parse::<f64>()
.context(format!("Failed to parse percentage: {}", pct_str))?;
eprintln!("â
Baseline coverage: {:.2}%", coverage);
return Ok(coverage);
}
}
}
anyhow::bail!("Could not find TOTAL line in coverage output")
}
async fn measure_coverage_gain(&self, previous_coverage: f64) -> Result<f64> {
eprintln!("đ Measuring coverage gain...");
let new_coverage = self.measure_baseline_coverage().await?;
let gain = new_coverage - previous_coverage;
if gain > 0.0 {
eprintln!("â
Coverage increased by {:.2}%", gain);
} else if gain < 0.0 {
eprintln!("â ī¸ Coverage decreased by {:.2}% (regression)", gain.abs());
} else {
eprintln!("âšī¸ No coverage change");
}
Ok(gain)
}
}