use crate::complexity::generate_complexity_histogram;
use tokmd_analysis_types::{ComplexityRisk, FileComplexity};
fn file_with_cyclomatic(path: &str, cyclomatic: usize) -> FileComplexity {
FileComplexity {
path: path.to_string(),
module: "src".to_string(),
function_count: 1,
max_function_length: 10,
cyclomatic_complexity: cyclomatic,
cognitive_complexity: None,
max_nesting: None,
risk_level: ComplexityRisk::Low,
functions: None,
}
}
#[test]
fn scenario_histogram_empty_input() {
let files: Vec<FileComplexity> = vec![];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.total, 0);
assert!(hist.counts.iter().all(|&c| c == 0));
assert_eq!(hist.buckets.len(), 7, "should have 7 buckets");
assert_eq!(hist.counts.len(), 7, "counts length matches buckets");
}
#[test]
fn scenario_histogram_single_low_complexity_file() {
let files = vec![file_with_cyclomatic("src/simple.rs", 3)];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.total, 1);
assert_eq!(hist.counts[0], 1, "bucket 0-4 should have 1 file");
assert_eq!(
hist.counts[1..].iter().sum::<u32>(),
0,
"other buckets empty"
);
}
#[test]
fn scenario_histogram_multiple_buckets() {
let files = vec![
file_with_cyclomatic("a.rs", 2), file_with_cyclomatic("b.rs", 7), file_with_cyclomatic("c.rs", 12), file_with_cyclomatic("d.rs", 18), file_with_cyclomatic("e.rs", 22), file_with_cyclomatic("f.rs", 28), file_with_cyclomatic("g.rs", 35), ];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.total, 7);
for (i, &count) in hist.counts.iter().enumerate() {
assert_eq!(count, 1, "bucket {i} should have exactly 1 file");
}
}
#[test]
fn scenario_histogram_high_complexity_clamps() {
let files = vec![file_with_cyclomatic("monster.rs", 999)];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.counts[6], 1, "last bucket should capture high values");
assert_eq!(hist.counts[..6].iter().sum::<u32>(), 0);
}
#[test]
fn scenario_histogram_boundary_values() {
let files = vec![
file_with_cyclomatic("at0.rs", 0),
file_with_cyclomatic("at4.rs", 4),
file_with_cyclomatic("at5.rs", 5),
file_with_cyclomatic("at9.rs", 9),
file_with_cyclomatic("at10.rs", 10),
file_with_cyclomatic("at29.rs", 29),
file_with_cyclomatic("at30.rs", 30),
];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.counts[0], 2, "0 and 4 in bucket 0-4");
assert_eq!(hist.counts[1], 2, "5 and 9 in bucket 5-9");
assert_eq!(hist.counts[2], 1, "10 in bucket 10-14");
assert_eq!(hist.counts[5], 1, "29 in bucket 25-29");
assert_eq!(hist.counts[6], 1, "30 in bucket 30+");
assert_eq!(hist.total, 7);
}
#[test]
fn scenario_histogram_bucket_labels() {
let files: Vec<FileComplexity> = vec![];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.buckets, vec![0, 5, 10, 15, 20, 25, 30]);
}
#[test]
fn scenario_histogram_custom_bucket_size() {
let files = vec![
file_with_cyclomatic("a.rs", 5),
file_with_cyclomatic("b.rs", 15),
file_with_cyclomatic("c.rs", 65),
];
let hist = generate_complexity_histogram(&files, 10);
assert_eq!(hist.buckets, vec![0, 10, 20, 30, 40, 50, 60]);
assert_eq!(hist.counts[0], 1, "5 in bucket 0-9");
assert_eq!(hist.counts[1], 1, "15 in bucket 10-19");
assert_eq!(hist.counts[6], 1, "65 clamped to last bucket");
assert_eq!(hist.total, 3);
}
#[test]
fn scenario_histogram_concentration_in_one_bucket() {
let files: Vec<FileComplexity> = (0..50)
.map(|i| file_with_cyclomatic(&format!("file{i}.rs"), 3))
.collect();
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.counts[0], 50);
assert_eq!(hist.total, 50);
assert_eq!(hist.counts[1..].iter().sum::<u32>(), 0);
}
#[test]
fn scenario_histogram_total_invariant() {
let files: Vec<FileComplexity> = (0..13)
.map(|i| file_with_cyclomatic(&format!("f{i}.rs"), i * 3))
.collect();
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(
hist.total,
files.len() as u32,
"total must equal input count"
);
assert_eq!(
hist.counts.iter().sum::<u32>(),
hist.total,
"sum of counts must equal total"
);
}
#[test]
fn scenario_zero_complexity_first_bucket() {
let files = vec![file_with_cyclomatic("empty.rs", 0)];
let hist = generate_complexity_histogram(&files, 5);
assert_eq!(hist.counts[0], 1);
assert_eq!(hist.total, 1);
}
#[test]
fn scenario_histogram_ignores_cognitive_and_nesting() {
let mut f = file_with_cyclomatic("rich.rs", 12);
f.cognitive_complexity = Some(30);
f.max_nesting = Some(5);
let hist = generate_complexity_histogram(&[f], 5);
assert_eq!(hist.counts[2], 1);
assert_eq!(hist.total, 1);
}
#[test]
fn scenario_histogram_with_function_details() {
use tokmd_analysis_types::FunctionComplexityDetail;
let mut f = file_with_cyclomatic("detailed.rs", 8);
f.functions = Some(vec![FunctionComplexityDetail {
name: "complex_fn".to_string(),
line_start: 1,
line_end: 20,
length: 20,
cyclomatic: 8,
cognitive: Some(12),
max_nesting: Some(3),
param_count: Some(2),
}]);
let hist = generate_complexity_histogram(&[f], 5);
assert_eq!(hist.counts[1], 1);
}
#[test]
fn scenario_histogram_risk_levels_independent() {
let mut low = file_with_cyclomatic("low.rs", 3);
low.risk_level = ComplexityRisk::Low;
let mut critical = file_with_cyclomatic("critical.rs", 3);
critical.risk_level = ComplexityRisk::Critical;
let hist = generate_complexity_histogram(&[low, critical], 5);
assert_eq!(hist.counts[0], 2);
}