1use super::matcher::MatchResult;
4use super::scanner::StepComment;
5use super::steps::{flatten_steps, AlgorithmStep};
6
7#[derive(Debug, Clone)]
9pub struct CoverageResult {
10 pub anchor: String,
11 pub total_steps: usize,
12 pub implemented: Vec<Vec<u32>>,
13 pub missing: Vec<Vec<u32>>,
14 pub warnings: usize,
15 pub reordered: usize,
16}
17
18impl CoverageResult {
19 pub fn implemented_count(&self) -> usize {
20 self.implemented.len()
21 }
22
23 pub fn summary(&self) -> String {
25 let mut parts = vec![format!(
26 "{}: {}/{} steps",
27 self.anchor,
28 self.implemented_count(),
29 self.total_steps
30 )];
31 if self.warnings > 0 {
32 let s = if self.warnings != 1 { "s" } else { "" };
33 parts.push(format!("{} warning{s}", self.warnings));
34 }
35 if self.reordered > 0 {
36 parts.push(format!("{} reordered", self.reordered));
37 }
38 parts.join(" | ")
39 }
40}
41
42fn longest_increasing_subsequence_length(seq: &[usize]) -> usize {
44 if seq.is_empty() {
45 return 0;
46 }
47 let mut tails: Vec<usize> = Vec::new();
48 for &val in seq {
49 match tails.binary_search(&val) {
50 Ok(_) => {} Err(pos) => {
52 if pos == tails.len() {
53 tails.push(val);
54 } else {
55 tails[pos] = val;
56 }
57 }
58 }
59 }
60 tails.len()
61}
62
63#[derive(Debug, Clone)]
65pub struct StepValidation {
66 pub step: StepComment,
67 pub result: MatchResult,
68 pub spec_text: String,
69 pub algo_anchor: String,
70}
71
72pub fn compute_coverage(
74 validations: &[StepValidation],
75 algo_steps: &[AlgorithmStep],
76 anchor: &str,
77) -> CoverageResult {
78 let flat = flatten_steps(algo_steps);
79 let total = flat.len();
80
81 let mut step_to_idx = std::collections::HashMap::new();
83 let mut all_numbers = std::collections::HashSet::new();
84 for (i, s) in flat.iter().enumerate() {
85 step_to_idx.insert(s.number.clone(), i);
86 all_numbers.insert(s.number.clone());
87 }
88
89 let mut implemented: Vec<Vec<u32>> = Vec::new();
90 let mut implemented_set = std::collections::HashSet::new();
91 let mut spec_order_indices: Vec<usize> = Vec::new();
92 let mut warnings = 0;
93
94 for v in validations {
95 let key = v.step.number.clone();
96 match v.result {
97 MatchResult::Exact | MatchResult::Fuzzy => {
98 if !implemented_set.contains(&key) {
99 implemented.push(key.clone());
100 implemented_set.insert(key.clone());
101 if let Some(&idx) = step_to_idx.get(&key) {
102 spec_order_indices.push(idx);
103 }
104 }
105 }
106 MatchResult::Mismatch => {
107 if !implemented_set.contains(&key) {
108 implemented.push(key.clone());
109 implemented_set.insert(key.clone());
110 if let Some(&idx) = step_to_idx.get(&key) {
111 spec_order_indices.push(idx);
112 }
113 }
114 warnings += 1;
115 }
116 MatchResult::NotFound => {
117 warnings += 1;
118 }
119 }
120 }
121
122 let missing: Vec<Vec<u32>> = flat
123 .iter()
124 .filter(|s| !implemented_set.contains(&s.number))
125 .map(|s| s.number.clone())
126 .collect();
127
128 let lis_len = longest_increasing_subsequence_length(&spec_order_indices);
129 let reordered = spec_order_indices.len().saturating_sub(lis_len);
130
131 CoverageResult {
132 anchor: anchor.to_string(),
133 total_steps: total,
134 implemented,
135 missing,
136 warnings,
137 reordered,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::analyze::steps::parse_steps;
145
146 const SIMPLE_ALGO: &str = "1. First.\n2. Second.\n3. Third.";
147 const NESTED_ALGO: &str = "1. Parent.\n\n 1. Child one.\n 2. Child two.\n2. Other.\n";
148
149 fn fake_validation(number: Vec<u32>, result: MatchResult) -> StepValidation {
150 StepValidation {
151 step: StepComment {
152 line: 0,
153 col_start: 0,
154 col_end: 10,
155 indent: 0,
156 number,
157 text: String::new(),
158 end_line: None,
159 },
160 result,
161 spec_text: String::new(),
162 algo_anchor: String::new(),
163 }
164 }
165
166 #[test]
169 fn lis_empty() {
170 assert_eq!(longest_increasing_subsequence_length(&[]), 0);
171 }
172
173 #[test]
174 fn lis_single() {
175 assert_eq!(longest_increasing_subsequence_length(&[5]), 1);
176 }
177
178 #[test]
179 fn lis_sorted() {
180 assert_eq!(longest_increasing_subsequence_length(&[1, 2, 3, 4, 5]), 5);
181 }
182
183 #[test]
184 fn lis_reverse() {
185 assert_eq!(longest_increasing_subsequence_length(&[5, 4, 3, 2, 1]), 1);
186 }
187
188 #[test]
189 fn lis_mixed() {
190 assert_eq!(longest_increasing_subsequence_length(&[1, 3, 2, 5]), 3);
191 }
192
193 #[test]
194 fn lis_duplicates() {
195 assert_eq!(longest_increasing_subsequence_length(&[1, 1, 1]), 1);
196 }
197
198 #[test]
199 fn lis_longer_sequence() {
200 assert_eq!(
201 longest_increasing_subsequence_length(&[3, 1, 4, 1, 5, 9, 2, 6]),
202 4
203 );
204 }
205
206 #[test]
209 fn all_exact() {
210 let steps = parse_steps(SIMPLE_ALGO);
211 let vals = vec![
212 fake_validation(vec![1], MatchResult::Exact),
213 fake_validation(vec![2], MatchResult::Exact),
214 fake_validation(vec![3], MatchResult::Exact),
215 ];
216 let cov = compute_coverage(&vals, &steps, "test");
217 assert_eq!(cov.total_steps, 3);
218 assert_eq!(cov.implemented_count(), 3);
219 assert!(cov.missing.is_empty());
220 assert_eq!(cov.warnings, 0);
221 assert_eq!(cov.reordered, 0);
222 }
223
224 #[test]
225 fn partial_coverage() {
226 let steps = parse_steps(SIMPLE_ALGO);
227 let vals = vec![
228 fake_validation(vec![1], MatchResult::Exact),
229 fake_validation(vec![3], MatchResult::Fuzzy),
230 ];
231 let cov = compute_coverage(&vals, &steps, "test");
232 assert_eq!(cov.total_steps, 3);
233 assert_eq!(cov.implemented_count(), 2);
234 assert_eq!(cov.missing, vec![vec![2u32]]);
235 assert_eq!(cov.warnings, 0);
236 }
237
238 #[test]
239 fn mismatch_counts_as_implemented_with_warning() {
240 let steps = parse_steps(SIMPLE_ALGO);
241 let vals = vec![
242 fake_validation(vec![1], MatchResult::Exact),
243 fake_validation(vec![2], MatchResult::Mismatch),
244 ];
245 let cov = compute_coverage(&vals, &steps, "test");
246 assert_eq!(cov.implemented_count(), 2);
247 assert_eq!(cov.warnings, 1);
248 assert_eq!(cov.missing, vec![vec![3u32]]);
249 }
250
251 #[test]
252 fn not_found_is_warning_only() {
253 let steps = parse_steps(SIMPLE_ALGO);
254 let vals = vec![
255 fake_validation(vec![1], MatchResult::Exact),
256 fake_validation(vec![99], MatchResult::NotFound),
257 ];
258 let cov = compute_coverage(&vals, &steps, "test");
259 assert_eq!(cov.implemented_count(), 1);
260 assert_eq!(cov.warnings, 1);
261 assert_eq!(cov.missing.len(), 2);
262 }
263
264 #[test]
265 fn reordered_detection() {
266 let steps = parse_steps(SIMPLE_ALGO);
267 let vals = vec![
268 fake_validation(vec![3], MatchResult::Exact),
269 fake_validation(vec![1], MatchResult::Exact),
270 fake_validation(vec![2], MatchResult::Exact),
271 ];
272 let cov = compute_coverage(&vals, &steps, "test");
273 assert_eq!(cov.implemented_count(), 3);
274 assert_eq!(cov.reordered, 1);
275 }
276
277 #[test]
278 fn no_validations() {
279 let steps = parse_steps(SIMPLE_ALGO);
280 let cov = compute_coverage(&[], &steps, "test");
281 assert_eq!(cov.total_steps, 3);
282 assert_eq!(cov.implemented_count(), 0);
283 assert_eq!(cov.missing.len(), 3);
284 assert_eq!(cov.warnings, 0);
285 assert_eq!(cov.reordered, 0);
286 }
287
288 #[test]
289 fn nested_coverage() {
290 let steps = parse_steps(NESTED_ALGO);
291 let vals = vec![
292 fake_validation(vec![1], MatchResult::Exact),
293 fake_validation(vec![1, 2], MatchResult::Fuzzy),
294 ];
295 let cov = compute_coverage(&vals, &steps, "test");
296 assert_eq!(cov.total_steps, 4);
297 assert_eq!(cov.implemented_count(), 2);
298 assert!(cov.missing.contains(&vec![1, 1]));
299 assert!(cov.missing.contains(&vec![2]));
300 }
301
302 #[test]
303 fn duplicate_step_counted_once() {
304 let steps = parse_steps(SIMPLE_ALGO);
305 let vals = vec![
306 fake_validation(vec![1], MatchResult::Exact),
307 fake_validation(vec![1], MatchResult::Exact),
308 fake_validation(vec![2], MatchResult::Exact),
309 ];
310 let cov = compute_coverage(&vals, &steps, "test");
311 assert_eq!(cov.implemented_count(), 2);
312 assert_eq!(cov.missing, vec![vec![3u32]]);
313 }
314
315 #[test]
318 fn summary_all_good() {
319 let cov = CoverageResult {
320 anchor: "navigate".into(),
321 total_steps: 23,
322 implemented: (1..=23).map(|i| vec![i]).collect(),
323 missing: vec![],
324 warnings: 0,
325 reordered: 0,
326 };
327 assert_eq!(cov.summary(), "navigate: 23/23 steps");
328 }
329
330 #[test]
331 fn summary_with_warnings() {
332 let cov = CoverageResult {
333 anchor: "navigate".into(),
334 total_steps: 23,
335 implemented: vec![vec![1], vec![2], vec![3]],
336 missing: (4..=23).map(|i| vec![i]).collect(),
337 warnings: 2,
338 reordered: 0,
339 };
340 assert_eq!(cov.summary(), "navigate: 3/23 steps | 2 warnings");
341 }
342
343 #[test]
344 fn summary_with_reordered() {
345 let cov = CoverageResult {
346 anchor: "navigate".into(),
347 total_steps: 10,
348 implemented: vec![vec![1], vec![2], vec![3]],
349 missing: vec![],
350 warnings: 0,
351 reordered: 1,
352 };
353 assert_eq!(cov.summary(), "navigate: 3/10 steps | 1 reordered");
354 }
355
356 #[test]
357 fn summary_with_all() {
358 let cov = CoverageResult {
359 anchor: "navigate".into(),
360 total_steps: 23,
361 implemented: vec![vec![1], vec![2]],
362 missing: vec![],
363 warnings: 1,
364 reordered: 2,
365 };
366 assert_eq!(
367 cov.summary(),
368 "navigate: 2/23 steps | 1 warning | 2 reordered"
369 );
370 }
371
372 #[test]
373 fn summary_singular_warning() {
374 let cov = CoverageResult {
375 anchor: "test".into(),
376 total_steps: 5,
377 implemented: vec![vec![1]],
378 missing: vec![],
379 warnings: 1,
380 reordered: 0,
381 };
382 let s = cov.summary();
383 assert!(s.contains("1 warning"));
384 assert!(!s.contains("warnings"));
385 }
386}