debtmap 0.20.0

Code complexity and technical debt analyzer
Documentation
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
use debtmap::priority::call_graph::{CallGraph, FunctionId};
/// Test case reproducing the LCOV coverage matching bug where functions with coverage
/// are incorrectly reported as having no coverage due to name/line matching failures.
///
/// Bug details:
/// - LCOV data contains function coverage (e.g., "read_requirements" with 577 executions)
/// - debtmap fails to match the function and reports it as uncovered
/// - This causes false positives in technical debt reporting
use debtmap::priority::coverage_propagation::calculate_coverage_urgency;
use debtmap::risk::lcov::parse_lcov_file;
use std::io::Write;
use std::path::PathBuf;
use tempfile::NamedTempFile;

/// Creates a realistic LCOV fixture mimicking the deku_string coverage data
fn create_lcov_fixture() -> String {
    r#"TN:
SF:/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs
FN:11,StringDeku::from_reader_with_ctx_impl
FN:59,StringDeku::to_writer_impl
FN:103,<impl DekuReader for StringDeku>::from_reader_with_ctx
FN:129,<impl DekuWriter for StringDeku>::to_writer
FN:153,read_requirements
FN:207,read_string
FN:245,write_string
FN:289,write_string_length_prefix
FN:328,write_string_fixed_length
FNF:9
FNDA:577,StringDeku::from_reader_with_ctx_impl
FNDA:565,StringDeku::to_writer_impl
FNDA:282,<impl DekuReader for StringDeku>::from_reader_with_ctx
FNDA:168,<impl DekuWriter for StringDeku>::to_writer
FNDA:577,read_requirements
FNDA:462,read_string
FNDA:565,write_string
FNDA:235,write_string_length_prefix
FNDA:196,write_string_fixed_length
DA:11,577
DA:20,517
DA:21,2368
DA:25,0
DA:27,55
DA:30,0
DA:31,0
DA:32,242
DA:33,261
DA:34,143
DA:37,0
DA:38,862
DA:39,174
DA:40,143
DA:43,0
DA:44,844
DA:45,252
DA:46,1480
DA:47,2428
DA:48,28
DA:50,0
DA:51,0
DA:53,56
DA:59,565
DA:66,565
DA:67,0
DA:68,191
DA:69,0
DA:71,0
DA:72,570
DA:73,950
DA:75,0
DA:76,368
DA:79,2100488
DA:81,920
DA:89,295
DA:96,1180
DA:97,1475
DA:103,282
DA:110,1128
DA:111,1410
DA:117,397
DA:122,1588
DA:123,2382
DA:129,168
DA:134,672
DA:135,1008
DA:153,577
DA:158,577
DA:159,0
DA:160,148
DA:161,0
DA:162,0
DA:163,0
DA:164,74
DA:166,74
DA:168,0
DA:169,0
DA:170,0
DA:171,0
DA:172,0
DA:175,0
DA:177,62
DA:178,139
DA:179,131
DA:180,130
DA:182,367
DA:183,674
DA:184,110
DA:185,270
DA:186,270
DA:187,0
DA:188,73
DA:189,315
DA:190,0
DA:193,0
DA:194,0
DA:195,0
DA:196,0
DA:197,0
DA:207,462
DA:245,565
DA:289,235
DA:299,235
DA:300,94
DA:301,94
DA:302,94
DA:303,47
DA:306,235
DA:307,0
DA:315,235
DA:316,94
DA:317,94
DA:318,94
DA:319,0
DA:320,47
DA:321,47
DA:324,235
DA:328,196
DA:341,196
DA:342,0
DA:347,196
DA:348,0
DA:353,196
DA:355,1764
DA:356,1764
DA:359,196
LF:104
LH:73
end_of_record
"#
    .to_string()
}

#[test]
fn test_lcov_coverage_matching_bug_exact_match() {
    // Create temporary LCOV file
    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    // Parse LCOV data
    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let file_path = PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs");

    // Test 1: Direct function name lookup should work
    let coverage = lcov_data.get_function_coverage(&file_path, "read_requirements");
    assert!(
        coverage.is_some(),
        "Should find coverage for 'read_requirements' by exact name"
    );

    // The function has 577 executions, so it should be considered covered
    // We don't know the exact percentage without line coverage details, but it should be > 0
    if let Some(cov) = coverage {
        assert!(
            cov > 0.0,
            "Function with 577 executions should have coverage > 0, got {}",
            cov
        );
    }
}

#[test]
fn test_lcov_coverage_matching_bug_with_line_bounds() {
    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let file_path = PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs");

    // Test 2: Function lookup with exact line boundaries
    // read_requirements starts at line 153 according to LCOV
    let coverage = lcov_data.get_function_coverage_with_bounds(
        &file_path,
        "read_requirements",
        153, // exact start line from LCOV
        200, // approximate end line
    );

    assert!(
        coverage.is_some(),
        "Should find coverage for 'read_requirements' with exact line bounds"
    );
}

#[test]
fn test_lcov_coverage_matching_bug_with_off_by_one_lines() {
    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let file_path = PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs");

    // Test 3: Function lookup with slightly off line numbers (common AST vs LCOV mismatch)
    // This simulates the case where AST reports line 152 or 154 but LCOV has 153
    let coverage_off_by_one = lcov_data.get_function_coverage_with_bounds(
        &file_path,
        "read_requirements",
        152, // off by 1 line (AST might report different line than LCOV)
        200,
    );

    // This currently FAILS but should succeed with tolerance
    // Uncomment when fix is implemented:
    // assert!(
    //     coverage_off_by_one.is_some(),
    //     "Should find coverage with ±1 line tolerance"
    // );

    // For now, document the bug:
    if coverage_off_by_one.is_none() {
        println!("BUG CONFIRMED: Function not found when line number is off by 1");
    }
}

#[test]
fn test_lcov_coverage_matching_bug_with_impl_blocks() {
    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let file_path = PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs");

    // Test 4: Functions with impl blocks and angle brackets
    let impl_coverage = lcov_data.get_function_coverage(
        &file_path,
        "<impl DekuReader for StringDeku>::from_reader_with_ctx",
    );

    assert!(
        impl_coverage.is_some(),
        "Should find coverage for impl block function with angle brackets"
    );

    // Test normalized version (what AST might provide)
    let normalized_coverage = lcov_data.get_function_coverage(
        &file_path,
        "_impl DekuReader for StringDeku_::from_reader_with_ctx",
    );

    // This might fail due to normalization issues
    if normalized_coverage.is_none() {
        println!("BUG: Normalized function name doesn't match LCOV data");
    }
}

#[test]
fn test_lcov_coverage_urgency_calculation_with_bug() {
    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let call_graph = CallGraph::new();

    // Test 5: Coverage urgency calculation for functions that ARE covered
    let func_id = FunctionId::new(
        PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs"),
        "read_requirements".to_string(),
        153,
    );

    let urgency = calculate_coverage_urgency(&func_id, &call_graph, &lcov_data, 12);

    // Function has 577 executions but only 48% line coverage,
    // so urgency should be moderate-to-high (around 6-7)
    // With complexity 12 and ~50% coverage gap, urgency ~7 is correct
    assert!(
        (6.0..8.0).contains(&urgency),
        "Function with 48% coverage and complexity 12 should have urgency 6-8, got {}",
        urgency
    );
}

#[test]
fn test_lcov_false_positive_detection() {
    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let file_path = PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs");

    // Test all three high-complexity functions reported by debtmap
    let test_cases = vec![
        ("read_requirements", 153, 577),          // Finding #1
        ("write_string_fixed_length", 328, 196),  // Finding #2
        ("write_string_length_prefix", 289, 235), // Finding #3
    ];

    for (func_name, line, expected_executions) in test_cases {
        // Check that function exists in LCOV with expected execution count
        let funcs = lcov_data.functions.get(&file_path).unwrap();
        let lcov_func = funcs.iter().find(|f| f.name == func_name);

        assert!(
            lcov_func.is_some(),
            "Function '{}' should exist in LCOV data",
            func_name
        );

        let lcov_func = lcov_func.unwrap();
        assert_eq!(
            lcov_func.execution_count, expected_executions,
            "Function '{}' should have {} executions",
            func_name, expected_executions
        );
        assert_eq!(
            lcov_func.start_line, line,
            "Function '{}' should start at line {}",
            func_name, line
        );

        // Now test that coverage lookup works
        let coverage = lcov_data.get_function_coverage(&file_path, func_name);
        assert!(
            coverage.is_some(),
            "Should find coverage for '{}' which has {} executions",
            func_name,
            expected_executions
        );

        // Test with bounds lookup (simulating what debtmap does)
        let coverage_with_bounds = lcov_data.get_function_coverage_with_bounds(
            &file_path,
            func_name,
            line,
            line + 50, // approximate function length
        );

        assert!(
            coverage_with_bounds.is_some(),
            "Should find coverage for '{}' with line bounds [{}, {}]",
            func_name,
            line,
            line + 50
        );
    }
}

#[test]
fn test_coverage_factor_scoring_bug() {
    // This test demonstrates the scoring bug where coverage_factor = 10.0
    // is displayed as "Coverage gap (40%)" which is misleading

    let lcov_content = create_lcov_fixture();
    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(lcov_content.as_bytes()).unwrap();

    let lcov_data = parse_lcov_file(temp_file.path()).unwrap();
    let call_graph = CallGraph::new();

    // Simulate what happens when function is not found
    let missing_func_id = FunctionId::new(
        PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs"),
        "nonexistent_function".to_string(),
        999,
    );

    let missing_urgency = calculate_coverage_urgency(&missing_func_id, &call_graph, &lcov_data, 10);

    // When function is not found, urgency can exceed 10.0 with spec 96
    assert!(
        missing_urgency >= 10.0,
        "Missing function should have urgency at least 10.0, got {}",
        missing_urgency
    );

    // Now test with a function that exists
    let existing_func_id = FunctionId::new(
        PathBuf::from("/Users/glen/memento-mori/deku_string/src/string/deku_impl.rs"),
        "read_requirements".to_string(),
        153,
    );

    let existing_urgency =
        calculate_coverage_urgency(&existing_func_id, &call_graph, &lcov_data, 12);

    // Function with coverage should have urgency < 10.0
    assert!(
        existing_urgency < 10.0,
        "Covered function should have urgency < 10.0, got {}",
        existing_urgency
    );

    println!(
        "Coverage urgency difference: missing={:.2}, existing={:.2}",
        missing_urgency, existing_urgency
    );
}