1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Coverage baseline measurement and coverage gain tracking
// Included into mod.rs via include!() -- no `use` imports or `#!` attributes allowed
/// Why the improvement loop should stop after `report`, if it should.
///
/// "No progress" means the iteration generated no test AND coverage did not
/// rise: the next iteration would run the same prioritisation over the same
/// tree and produce the same report, so continuing only costs another full
/// coverage build. Returns `None` while there is any reason to keep going.
fn zero_progress_stop_reason(report: &IterationReport) -> Option<String> {
if report.tests_generated == 0 && report.coverage_gain <= 0.0 {
Some(format!(
"No progress in iteration {}: 0 tests generated and {:+.2}% coverage change; \
further iterations would repeat it",
report.iteration, report.coverage_gain
))
} else {
None
}
}
impl CoverageImprovementService {
/// Improve coverage to target percentage
///
/// Returns a report of all iterations and final coverage achieved.
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn improve_coverage(&self) -> Result<CoverageImprovementReport> {
// Phase 1: Measure baseline
let baseline = self.measure_baseline_coverage().await?;
// Check if already at target
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();
// Phase 2-5: Iterate until target reached or max iterations
for iteration in 1..=self.config.max_iterations {
// Check if we've reached target
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),
});
}
// Run one iteration
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;
let stalled = zero_progress_stop_reason(&iteration_report);
iterations.push(iteration_report);
// An iteration that generated no test and moved coverage nowhere is
// deterministic: repeating it produces byte-identical results. The
// loop had no stop condition other than the target, so
// `--max-iterations 10` on a project with no generatable targets
// printed ten identical zero-gain iterations (and re-ran the whole
// coverage build ten times) before stopping.
if let Some(stop_reason) = stalled {
return 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,
});
}
}
// Max iterations reached
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),
})
}
/// Measure baseline coverage using cargo-llvm-cov
async fn measure_baseline_coverage(&self) -> Result<f64> {
crate::status_eprintln!("đ Running coverage analysis...");
// Find directory containing Makefile (search current and parent directories)
let makefile_dir = self.find_makefile_directory()?;
crate::status_eprintln!(" đ Running from: {}", makefile_dir.display());
// Run make coverage
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
);
}
// Parse stdout to find TOTAL line and extract coverage percentage
let stdout = String::from_utf8_lossy(&output.stdout);
Self::parse_coverage_percentage(&stdout)
.context("Failed to parse coverage from make coverage output")
}
/// Find the directory containing Makefile
fn find_makefile_directory(&self) -> Result<PathBuf> {
let mut current = self.config.project_path.clone();
// Resolve to absolute path
if current.is_relative() {
current = std::env::current_dir()?.join(¤t);
}
current = current.canonicalize().unwrap_or(current);
// Search up to 5 parent directories
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()
)
}
/// Parse coverage percentage from make coverage output
///
/// Example TOTAL line:
/// `TOTAL 241150 203105 15.78% 17533 14596 16.75% 173884 145810 16.15% 0 0 -`
///
/// We extract the last percentage before the dash (line coverage)
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn parse_coverage_percentage(output: &str) -> Result<f64> {
for line in output.lines() {
if line.trim().starts_with("TOTAL") {
// Split by whitespace and find all percentages
let parts: Vec<&str> = line.split_whitespace().collect();
// Find all percentage values (contain '%')
let percentages: Vec<&str> =
parts.iter().filter(|s| s.contains('%')).copied().collect();
// The last percentage is line coverage
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))?;
crate::status_eprintln!("â
Baseline coverage: {:.2}%", coverage);
return Ok(coverage);
}
}
}
anyhow::bail!("Could not find TOTAL line in coverage output")
}
/// Measure coverage gain from this iteration
///
/// Re-runs coverage analysis and calculates the delta from the previous coverage.
/// Handles edge cases like coverage decrease (negative gain) and no change (zero gain).
async fn measure_coverage_gain(&self, previous_coverage: f64) -> Result<f64> {
crate::status_eprintln!("đ Measuring coverage gain...");
// Measure current coverage after test generation
let new_coverage = self.measure_baseline_coverage().await?;
// Calculate delta
let gain = new_coverage - previous_coverage;
// Log the gain
if gain > 0.0 {
crate::status_eprintln!("â
Coverage increased by {:.2}%", gain);
} else if gain < 0.0 {
eprintln!("â ī¸ Coverage decreased by {:.2}% (regression)", gain.abs());
} else {
crate::status_eprintln!("âšī¸ No coverage change");
}
Ok(gain)
}
}