use proptest::prelude::*;
use crate::complexity::generate_complexity_histogram;
use tokmd_analysis_types::{ComplexityRisk, FileComplexity};
fn arb_risk() -> impl Strategy<Value = ComplexityRisk> {
prop_oneof![
Just(ComplexityRisk::Low),
Just(ComplexityRisk::Moderate),
Just(ComplexityRisk::High),
Just(ComplexityRisk::Critical),
]
}
fn arb_file_complexity() -> impl Strategy<Value = FileComplexity> {
(
"[a-z]{1,8}\\.rs", "[a-z]{1,5}", 0..200usize, 0..500usize, 0..500usize, proptest::option::of(0..300usize), proptest::option::of(0..20usize), arb_risk(),
)
.prop_map(
|(path, module, fc, mfl, cc, cog, nest, risk)| FileComplexity {
path,
module,
function_count: fc,
max_function_length: mfl,
cyclomatic_complexity: cc,
cognitive_complexity: cog,
max_nesting: nest,
risk_level: risk,
functions: None,
},
)
}
fn arb_file_vec() -> impl Strategy<Value = Vec<FileComplexity>> {
proptest::collection::vec(arb_file_complexity(), 0..50)
}
proptest! {
#[test]
fn prop_histogram_total_equals_file_count(files in arb_file_vec()) {
let hist = generate_complexity_histogram(&files, 5);
prop_assert_eq!(hist.total, files.len() as u32);
}
}
proptest! {
#[test]
fn prop_histogram_counts_sum_to_total(files in arb_file_vec()) {
let hist = generate_complexity_histogram(&files, 5);
let sum: u32 = hist.counts.iter().sum();
prop_assert_eq!(sum, hist.total);
}
}
proptest! {
#[test]
fn prop_histogram_always_7_buckets(files in arb_file_vec()) {
let hist = generate_complexity_histogram(&files, 5);
prop_assert_eq!(hist.buckets.len(), 7);
prop_assert_eq!(hist.counts.len(), 7);
}
}
proptest! {
#[test]
fn prop_histogram_buckets_monotonic(
files in arb_file_vec(),
bucket_size in 1u32..20,
) {
let hist = generate_complexity_histogram(&files, bucket_size);
for window in hist.buckets.windows(2) {
prop_assert!(window[0] < window[1], "buckets must be strictly increasing");
}
}
}
proptest! {
#[test]
fn prop_histogram_empty_all_zeros(bucket_size in 1u32..100) {
let hist = generate_complexity_histogram(&[], bucket_size);
prop_assert_eq!(hist.total, 0);
prop_assert!(hist.counts.iter().all(|&c| c == 0));
}
}
proptest! {
#[test]
fn prop_single_file_one_bucket(file in arb_file_complexity()) {
let hist = generate_complexity_histogram(&[file], 5);
let nonzero_buckets = hist.counts.iter().filter(|&&c| c > 0).count();
prop_assert_eq!(nonzero_buckets, 1, "single file must be in exactly one bucket");
prop_assert_eq!(hist.total, 1);
}
}
proptest! {
#[test]
fn prop_histogram_deterministic(files in arb_file_vec()) {
let h1 = generate_complexity_histogram(&files, 5);
let h2 = generate_complexity_histogram(&files, 5);
prop_assert_eq!(h1.buckets, h2.buckets);
prop_assert_eq!(h1.counts, h2.counts);
prop_assert_eq!(h1.total, h2.total);
}
}
proptest! {
#[test]
fn prop_no_bucket_exceeds_total(files in arb_file_vec()) {
let hist = generate_complexity_histogram(&files, 5);
for (i, &count) in hist.counts.iter().enumerate() {
prop_assert!(
count <= hist.total,
"bucket {} count {} exceeds total {}", i, count, hist.total,
);
}
}
}
proptest! {
#[test]
fn prop_bucket_boundaries_correct(bucket_size in 1u32..50) {
let hist = generate_complexity_histogram(&[], bucket_size);
for (i, &b) in hist.buckets.iter().enumerate() {
prop_assert_eq!(b, (i as u32) * bucket_size);
}
}
}
proptest! {
#[test]
fn prop_adding_file_monotone(
files in arb_file_vec(),
extra in arb_file_complexity(),
) {
let h_before = generate_complexity_histogram(&files, 5);
let mut extended = files.clone();
extended.push(extra);
let h_after = generate_complexity_histogram(&extended, 5);
prop_assert_eq!(h_after.total, h_before.total + 1);
for (i, (&before, &after)) in h_before.counts.iter().zip(h_after.counts.iter()).enumerate() {
prop_assert!(
after >= before,
"bucket {} decreased from {} to {}", i, before, after,
);
}
}
}
proptest! {
#[test]
fn prop_same_cyclomatic_same_bucket(
cyclo in 0..200usize,
count in 1..10usize,
) {
let files: Vec<FileComplexity> = (0..count)
.map(|i| {
FileComplexity {
path: format!("f{i}.rs"),
module: "src".to_string(),
function_count: 1,
max_function_length: 5,
cyclomatic_complexity: cyclo,
cognitive_complexity: None,
max_nesting: None,
risk_level: ComplexityRisk::Low,
functions: None,
}
})
.collect();
let hist = generate_complexity_histogram(&files, 5);
let nonzero: Vec<usize> = hist.counts.iter().enumerate()
.filter(|(_, c)| **c > 0)
.map(|(i, _)| i)
.collect();
prop_assert_eq!(nonzero.len(), 1, "all files with same cyclomatic should be in one bucket");
prop_assert_eq!(hist.counts[nonzero[0]], count as u32);
}
}