pmat 3.16.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Property-based tests for quality check functions
//!
//! These tests verify invariants and properties of the quality checking system
//! using proptest for comprehensive coverage.

use crate::cli::analysis_utilities::{
    check_complexity, check_dead_code, check_duplicates, check_satd, QualityViolation,
};
use proptest::prelude::*;
use std::path::Path;
use tempfile::TempDir;
use tokio::runtime::Runtime;

/// Helper to create test files with specific content
fn create_test_file(dir: &Path, name: &str, content: &str) -> std::path::PathBuf {
    let file_path = dir.join(name);
    std::fs::write(&file_path, content).unwrap();
    file_path
}

proptest! {
    /// Property: Complexity violations only occur when cyclomatic complexity exceeds threshold
    #[test]
    fn prop_complexity_threshold_respected(
        threshold in 1u32..100u32,
        file_count in 1usize..10usize,
    ) {
        let rt = Runtime::new().unwrap();
        let temp_dir = TempDir::new().unwrap();

        // Create test files with varying complexity
        for i in 0..file_count {
            let complexity = i as u32 * 10; // 0, 10, 20, 30...
            let content = generate_code_with_complexity(complexity);
            create_test_file(temp_dir.path(), &format!("test{}.rs", i), &content);
        }

        let _ = rt.block_on(async {
            let violations = check_complexity(temp_dir.path(), threshold).await.unwrap();

            // Verify all violations are for files with complexity issues
            for violation in violations {
                assert_eq!(violation.check_type, "complexity");

                // Extract complexity from message
                if let Some(complexity_str) = violation.message
                    .split("complexity ")
                    .nth(1)
                    .and_then(|s| s.split(' ').next())
                    .and_then(|s| s.parse::<u32>().ok()) {
                    // Warnings are for complexity > (threshold - 2) but <= threshold
                    // Errors are for complexity > threshold
                    if violation.severity == "warning" {
                        let warn_threshold = threshold.saturating_sub(2).max(1);
                        prop_assert!(complexity_str > warn_threshold);
                        prop_assert!(complexity_str <= threshold);
                    } else if violation.severity == "error" {
                        prop_assert!(complexity_str > threshold);
                    } else {
                        panic!("Unexpected severity: {}", violation.severity);
                    }
                }
            }
            Ok(())
        });
    }

    /// Property: Dead code percentage calculations are accurate
    #[test]
    fn prop_dead_code_percentage_accurate(
        max_percentage in 0.0..50.0,
        total_functions in 10usize..100usize,
        dead_ratio in 0.0..0.5f64,
    ) {
        let rt = Runtime::new().unwrap();
        let temp_dir = TempDir::new().unwrap();

        // Create a Rust file with known dead/live function ratio
        let dead_count = (total_functions as f64 * dead_ratio) as usize;
        let mut content = String::new();

        // Add live functions
        for i in 0..(total_functions - dead_count) {
            content.push_str(&format!("pub fn live_func_{}() {{ println!(\"live\"); }}\n", i));
        }

        // Add dead functions (private, prefixed with _)
        for i in 0..dead_count {
            content.push_str(&format!("fn _dead_func_{}() {{ println!(\"dead\"); }}\n", i));
        }

        create_test_file(temp_dir.path(), "lib.rs", &content);

        let _ = rt.block_on(async {
            let violations = check_dead_code(temp_dir.path(), max_percentage).await.unwrap();

            // Property: Violations occur only when dead code exceeds threshold
            let expected_percentage = (dead_count as f64 / total_functions as f64) * 100.0;
            if expected_percentage > max_percentage {
                prop_assert!(!violations.is_empty(),
                    "Expected violations when dead code {}% > max {}%",
                    expected_percentage, max_percentage);
            }
            Ok(())
        });
    }

    /// Property: SATD detection finds all and only valid patterns
    #[test]
    fn prop_satd_detection_complete_and_sound(
        satd_types in prop::collection::vec(
            prop_oneof![
                Just("TODO"),
                Just("FIXME"),
                Just("HACK"),
                Just("XXX"),
                Just("BUG"),
                Just("REFACTOR"),
            ],
            0..10
        ),
        descriptions in prop::collection::vec("[a-zA-Z0-9 ]{1,50}", 0..10),
    ) {
        let rt = Runtime::new().unwrap();
        let temp_dir = TempDir::new().unwrap();

        // Create file with known SATD comments
        let mut content = String::new();
        let expected_count = satd_types.len().min(descriptions.len());

        for i in 0..expected_count {
            content.push_str(&format!("// {}: {}\n", satd_types[i], descriptions[i]));
            content.push_str("fn some_function() {}\n\n");
        }

        // Add some non-SATD comments
        content.push_str("// This is a regular comment\n");
        content.push_str("// Another normal comment without debt markers\n");

        create_test_file(temp_dir.path(), "test.rs", &content);

        let _ = rt.block_on(async {
            let violations = check_satd(temp_dir.path()).await.unwrap();

            // Property: Number of violations matches number of SATD comments
            prop_assert_eq!(violations.len(), expected_count);

            // Property: All violations are SATD type with line numbers
            for violation in &violations {
                prop_assert_eq!(&violation.check_type, "satd");
                prop_assert!(violation.line.is_some());
                prop_assert!(violation.severity == "warning" || violation.severity == "error");
            }
            Ok(())
        });
    }

    /// Property: Duplicate detection is symmetric and transitive
    #[test]
    fn prop_duplicate_detection_properties(
        file_contents in prop::collection::vec(
            "[a-zA-Z0-9\n ]{50,200}", // Generate code-like content
            2..10
        ),
        duplicate_indices in prop::collection::vec(0usize..10usize, 0..5),
    ) {
        let rt = Runtime::new().unwrap();
        let temp_dir = TempDir::new().unwrap();

        // Create files, some with duplicate content
        let mut duplicate_groups = std::collections::HashMap::new();

        for (i, content) in file_contents.iter().enumerate() {
            let file_content = if duplicate_indices.contains(&i) && i > 0 {
                // Make this a duplicate of the first file
                duplicate_groups.entry(0).or_insert_with(Vec::new).push(i);
                &file_contents[0]
            } else {
                duplicate_groups.entry(i).or_insert_with(Vec::new);
                content
            };

            create_test_file(temp_dir.path(), &format!("file{}.rs", i), file_content);
        }

        let _ = rt.block_on(async {
            let violations = check_duplicates(temp_dir.path()).await.unwrap();

            // Property: Duplicates are detected symmetrically
            let mut violation_pairs = std::collections::HashSet::new();
            for violation in &violations {
                violation_pairs.insert(violation.file.clone());
            }

            // If A is duplicate of B, then B should be duplicate of A
            for violation in &violations {
                if let Some(other_files) = extract_files_from_duplicate_message(&violation.message) {
                    for other_file in other_files {
                        prop_assert!(
                            violation_pairs.contains(&other_file),
                            "Symmetry violated: {} marked as duplicate but {} is not",
                            violation.file, other_file
                        );
                    }
                }
            }
            Ok(())
        });
    }
}

/// Helper to generate code with specific cyclomatic complexity
fn generate_code_with_complexity(target_complexity: u32) -> String {
    let mut code = String::from("fn complex_function() {\n");

    // Each if statement adds 1 to cyclomatic complexity
    for i in 0..target_complexity {
        code.push_str(&format!("    if condition_{} {{\n", i));
        code.push_str(&format!("        println!(\"branch {}\");\n", i));
        code.push_str("    }\n");
    }

    code.push_str("}\n");
    code
}

/// Extract file names from duplicate violation message
fn extract_files_from_duplicate_message(message: &str) -> Option<Vec<String>> {
    message
        .split("found in: ")
        .nth(1)
        .map(|files_part| files_part.split(", ").map(String::from).collect())
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod additional_property_tests {
    use super::*;

    proptest! {
        /// Property: Quality violations have consistent structure
        #[test]
        fn prop_violation_structure_valid(
            check_type in "[a-z_]+",
            severity in prop_oneof![Just("error"), Just("warning"), Just("info")],
            file_path in "[a-zA-Z0-9/_]+\\.rs",
            line in prop::option::of(1usize..10000usize),
            message in "[a-zA-Z0-9 :,.-]+",
        ) {
            let violation = QualityViolation::new(
                check_type.clone(),
                severity.to_string(),
                file_path.clone(),
                line,
                message.clone(),
            );

            // Properties that should always hold
            prop_assert!(!violation.check_type.is_empty());
            prop_assert!(!violation.severity.is_empty());
            prop_assert!(!violation.file.is_empty());
            prop_assert!(!violation.message.is_empty());

            // Severity should be one of the valid values
            prop_assert!(
                ["error", "warning", "info"].contains(&violation.severity.as_str()),
                "Invalid severity: {}", violation.severity
            );
        }
    }
}

/// Unit tests for quality gate check display and performance metrics functionality
/// These tests specifically verify the fixes for issues #30 and #31.
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod unit_tests {
    use super::*;
    use crate::cli::QualityCheckType;

    /// Test that quality check types are properly defined (Issue #30 support)
    #[test]
    fn test_quality_check_types_comprehensive() {
        // Test that all check types can be created and cloned
        let all_checks = vec![
            QualityCheckType::All,
            QualityCheckType::Complexity,
            QualityCheckType::DeadCode,
            QualityCheckType::Satd,
            QualityCheckType::Security,
            QualityCheckType::Entropy,
            QualityCheckType::Duplicates,
            QualityCheckType::Coverage,
            QualityCheckType::Sections,
            QualityCheckType::Provability,
        ];

        // All check types should be valid
        assert_eq!(
            all_checks.len(),
            10,
            "Should have exactly 10 check types defined"
        );

        // Each check type should be cloneable and comparable
        for check_type in &all_checks {
            let cloned = check_type.clone();
            assert_eq!(
                check_type, &cloned,
                "Check type should be cloneable and comparable"
            );
        }
    }

    /// Test check type string representation consistency (Issue #30)
    #[test]
    fn test_check_type_debug_format() {
        let complexity_check = QualityCheckType::Complexity;
        let debug_string = format!("{:?}", complexity_check);

        // Debug format should contain the type name
        assert!(
            debug_string.contains("Complexity"),
            "Debug format should contain type name"
        );
        assert!(!debug_string.is_empty(), "Debug format should not be empty");
    }

    /// Test performance metrics calculation and display (Issue #31)
    #[test]
    fn test_performance_metrics_calculation() {
        use std::time::Duration;

        // Test timing calculation
        let start_time = std::time::Instant::now();
        std::thread::sleep(Duration::from_millis(10)); // Small delay for testing
        let elapsed = start_time.elapsed();

        // Basic properties of elapsed time
        assert!(
            elapsed.as_millis() >= 10,
            "Should have elapsed at least 10ms"
        );
        assert!(
            elapsed.as_millis() < 1000,
            "Should have elapsed less than 1 second"
        );

        // Test performance metrics formatting
        let num_checks = 5;
        let avg_time = elapsed.as_secs_f64() / num_checks as f64;

        assert!(avg_time > 0.0, "Average time should be positive");
        assert!(avg_time.is_finite(), "Average time should be finite");
    }

    /// Test performance metrics display format (Issue #31)
    #[test]
    fn test_performance_metrics_display_format() {
        use std::time::Duration;

        let total_time = Duration::from_millis(1500); // 1.5 seconds
        let num_checks = 3;
        let avg_time = total_time.as_secs_f64() / num_checks as f64;

        // Format performance output (similar to what's in quality gate)
        let perf_output = format!(
            "⏱️  Performance Metrics:\n  Total execution time: {:.2}s\n  Checks performed: {}\n  Average time per check: {:.2}s",
            total_time.as_secs_f64(),
            num_checks,
            avg_time
        );

        // Verify format contains expected elements
        assert!(perf_output.contains("Performance Metrics"));
        assert!(perf_output.contains("Total execution time: 1.50s"));
        assert!(perf_output.contains("Checks performed: 3"));
        assert!(perf_output.contains("Average time per check: 0.50s"));
    }

    /// Test that performance flag integration works as expected
    #[test]
    fn test_performance_flag_integration() {
        // Test with performance enabled
        let perf_enabled = true;
        let should_show_metrics = perf_enabled;
        assert!(
            should_show_metrics,
            "Performance metrics should be shown when perf flag is enabled"
        );

        // Test with performance disabled
        let perf_disabled = false;
        let should_show_metrics = perf_disabled;
        assert!(
            !should_show_metrics,
            "Performance metrics should not be shown when perf flag is disabled"
        );
    }

    /// Test check name generation consistency (supporting Issue #30)
    #[test]
    fn test_check_type_consistency() {
        // Check that the same check type always produces the same results
        let complexity_check1 = QualityCheckType::Complexity;
        let complexity_check2 = QualityCheckType::Complexity;

        // Should be equal
        assert_eq!(
            complexity_check1, complexity_check2,
            "Same check types should be equal"
        );

        // Should have same debug representation
        assert_eq!(
            format!("{:?}", complexity_check1),
            format!("{:?}", complexity_check2),
            "Same check types should have identical debug representation"
        );
    }

    /// Test violation detection with complexity check
    #[test]
    fn test_complexity_violation_detection() {
        let rt = Runtime::new().unwrap();
        let temp_dir = TempDir::new().unwrap();

        // Create a test file with complexity exceeding built-in threshold (cyclomatic > 30).
        // check_complexity() reads thresholds from config service, not the passed parameter.
        // Use if-else chains (not match arms) since each `if` adds 1 to cyclomatic complexity.
        let branches: String = (0..35)
            .map(|i| format!("    if a == {i} {{ return {i}; }}\n"))
            .collect();
        let code = format!("fn f(a: i32) -> i32 {{\n{branches}    0\n}}");
        let _test_file = create_test_file(temp_dir.path(), "test.rs", &code);

        rt.block_on(async {
            let complexity_violations = check_complexity(temp_dir.path(), 5).await.unwrap();
            // 35 if-branches → cyclomatic ~36 > threshold 30
            assert!(
                !complexity_violations.is_empty(),
                "Should detect complexity violations when cyclomatic > 30"
            );

            for violation in &complexity_violations {
                assert_eq!(violation.check_type, "complexity");
                assert!(!violation.message.is_empty());
                assert!(!violation.file.is_empty());
            }
        });
    }
}