use crate::services::complexity::{ComplexityMetrics, FileComplexityMetrics, FunctionComplexity};
use proptest::prelude::*;
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
proptest! {
#[test]
fn prop_files_below_threshold_filtered_out(
threshold in 20u16..100,
num_files in 1usize..10,
functions_per_file in 1usize..5,
) {
let mut files = vec![];
for i in 0..num_files {
let file = FileComplexityMetrics {
path: format!("file_{}.rs", i),
total_complexity: ComplexityMetrics::new(threshold - 1, threshold - 1, 3, 100),
functions: (0..functions_per_file)
.map(|j| FunctionComplexity {
name: format!("func_{}", j),
line_start: j as u32 * 10,
line_end: (j as u32 + 1) * 10,
metrics: ComplexityMetrics::new((threshold - 1 - j as u16).max(1), (threshold - 1 - j as u16).max(1), 2, 10),
})
.collect(),
classes: vec![],
};
files.push(file);
}
let _max_cyclomatic = Some(threshold);
let filtered = files.into_iter().filter(|file| {
file.functions.iter().any(|func| {
func.metrics.cyclomatic > threshold
})
}).collect::<Vec<_>>();
prop_assert_eq!(filtered.len(), 0);
}
#[test]
fn prop_files_above_threshold_kept(
threshold in 20u16..50,
num_files in 1usize..10,
high_complexity in 51u16..100,
) {
let mut files = vec![];
for i in 0..num_files {
let file = FileComplexityMetrics {
path: format!("file_{}.rs", i),
total_complexity: ComplexityMetrics::new(high_complexity, high_complexity, 5, 200),
functions: vec![
FunctionComplexity {
name: "complex_func".to_string(),
line_start: 1,
line_end: 50,
metrics: ComplexityMetrics::new(high_complexity, high_complexity, 5, 50),
},
FunctionComplexity {
name: "simple_func".to_string(),
line_start: 51,
line_end: 60,
metrics: ComplexityMetrics::new(5, 3, 1, 10),
},
],
classes: vec![],
};
files.push(file);
}
let _max_cyclomatic = Some(threshold);
let filtered = files.into_iter().filter(|file| {
file.functions.iter().any(|func| {
func.metrics.cyclomatic > threshold
})
}).collect::<Vec<_>>();
prop_assert_eq!(filtered.len(), num_files);
}
#[test]
fn prop_threshold_boundary_behavior(
threshold in 10u16..50,
) {
let files = vec![
FileComplexityMetrics {
path: "at_threshold.rs".to_string(),
total_complexity: ComplexityMetrics::new(threshold, threshold, 3, 100),
functions: vec![FunctionComplexity {
name: "at_threshold_func".to_string(),
line_start: 1,
line_end: 50,
metrics: ComplexityMetrics::new(threshold, threshold, 3, 50),
}],
classes: vec![],
},
FileComplexityMetrics {
path: "above_threshold.rs".to_string(),
total_complexity: ComplexityMetrics::new(threshold + 1, threshold + 1, 3, 100),
functions: vec![FunctionComplexity {
name: "above_threshold_func".to_string(),
line_start: 1,
line_end: 50,
metrics: ComplexityMetrics::new(threshold + 1, threshold + 1, 3, 50),
}],
classes: vec![],
},
];
let _max_cyclomatic = Some(threshold);
let filtered = files.into_iter().filter(|file| {
file.functions.iter().any(|func| {
func.metrics.cyclomatic > threshold
})
}).collect::<Vec<_>>();
prop_assert_eq!(filtered.len(), 1);
prop_assert_eq!(&filtered[0].path, "above_threshold.rs");
}
#[test]
fn prop_independent_threshold_filtering(
cyc_threshold in 20u16..40,
cog_threshold in 30u16..50,
) {
let files = vec![
FileComplexityMetrics {
path: "high_cyclomatic.rs".to_string(),
total_complexity: ComplexityMetrics::new(cyc_threshold + 10, cog_threshold - 10, 3, 100),
functions: vec![FunctionComplexity {
name: "cyc_complex".to_string(),
line_start: 1,
line_end: 50,
metrics: ComplexityMetrics::new(cyc_threshold + 10, cog_threshold - 10, 3, 50),
}],
classes: vec![],
},
FileComplexityMetrics {
path: "high_cognitive.rs".to_string(),
total_complexity: ComplexityMetrics::new(cyc_threshold - 10, cog_threshold + 10, 3, 100),
functions: vec![FunctionComplexity {
name: "cog_complex".to_string(),
line_start: 1,
line_end: 50,
metrics: ComplexityMetrics::new(cyc_threshold - 10, cog_threshold + 10, 3, 50),
}],
classes: vec![],
},
FileComplexityMetrics {
path: "simple.rs".to_string(),
total_complexity: ComplexityMetrics::new(cyc_threshold - 10, cog_threshold - 10, 1, 50),
functions: vec![FunctionComplexity {
name: "simple_func".to_string(),
line_start: 1,
line_end: 25,
metrics: ComplexityMetrics::new(cyc_threshold - 10, cog_threshold - 10, 1, 25),
}],
classes: vec![],
},
];
let cyc_filtered = files.clone().into_iter().filter(|file| {
file.functions.iter().any(|func| {
func.metrics.cyclomatic > cyc_threshold
})
}).collect::<Vec<_>>();
prop_assert_eq!(cyc_filtered.len(), 1);
prop_assert_eq!(&cyc_filtered[0].path, "high_cyclomatic.rs");
let cog_filtered = files.clone().into_iter().filter(|file| {
file.functions.iter().any(|func| {
func.metrics.cognitive > cog_threshold
})
}).collect::<Vec<_>>();
prop_assert_eq!(cog_filtered.len(), 1);
prop_assert_eq!(&cog_filtered[0].path, "high_cognitive.rs");
let both_filtered = files.into_iter().filter(|file| {
file.functions.iter().any(|func| {
func.metrics.cyclomatic > cyc_threshold ||
func.metrics.cognitive > cog_threshold
})
}).collect::<Vec<_>>();
prop_assert_eq!(both_filtered.len(), 2);
}
}
use crate::cli::handlers::complexity_handlers::ComplexityConfig;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_complexity_config_creation() {
let config = ComplexityConfig::from_args(
PathBuf::from("/test/project"),
Some("rust".to_string()),
Some(15),
Some(20),
vec!["**/*.rs".to_string()],
30,
10,
);
assert_eq!(config.project_path, PathBuf::from("/test/project"));
assert_eq!(config.toolchain, Some("rust".to_string()));
assert_eq!(config.max_cyclomatic, 15);
assert_eq!(config.max_cognitive, 20);
assert_eq!(config.include, vec!["**/*.rs".to_string()]);
assert_eq!(config.timeout, 30);
assert_eq!(config.top_files, 10);
}
#[test]
fn test_complexity_config_defaults() {
let config = ComplexityConfig::from_args(
PathBuf::from("/test"),
None,
None, None, vec![],
60,
5,
);
assert_eq!(config.max_cyclomatic, 10); assert_eq!(config.max_cognitive, 15); assert_eq!(config.toolchain, None);
}
#[test]
fn test_complexity_config_detect_toolchain() {
let config = ComplexityConfig::from_args(
PathBuf::from("/test"),
Some("python".to_string()),
Some(10),
Some(15),
vec![],
30,
5,
);
let detected = config.detect_toolchain();
assert_eq!(detected, Some("python".to_string()));
}
#[test]
fn test_complexity_config_detect_toolchain_none() {
let config = ComplexityConfig::from_args(
PathBuf::from("/test"),
None,
Some(10),
Some(15),
vec![],
30,
5,
);
let detected = config.detect_toolchain();
assert!(detected.is_none() || detected.is_some());
}
#[tokio::test]
async fn test_analyze_single_file_nonexistent() {
let config = ComplexityConfig::from_args(
PathBuf::from("/tmp"),
Some("rust".to_string()),
Some(10),
Some(15),
vec![],
30,
5,
);
let nonexistent_file = PathBuf::from("nonexistent.rs");
let result = super::super::analyze_single_file(&nonexistent_file, &config).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("File not found"));
}
#[tokio::test]
async fn test_analyze_single_file_absolute_path() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
std::fs::write(&test_file, "fn main() { println!(\"Hello\"); }").unwrap();
let config = ComplexityConfig::from_args(
temp_dir.path().to_path_buf(),
Some("rust".to_string()),
Some(10),
Some(15),
vec![],
30,
5,
);
let result = super::super::analyze_single_file(&test_file, &config).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_analyze_multiple_files_empty() {
let config = ComplexityConfig::from_args(
PathBuf::from("/tmp"),
Some("rust".to_string()),
Some(10),
Some(15),
vec![],
30,
5,
);
let empty_files: Vec<PathBuf> = vec![];
let result = super::super::analyze_multiple_files(&empty_files, &config).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[tokio::test]
async fn test_analyze_multiple_files_with_nonexistent() {
let config = ComplexityConfig::from_args(
PathBuf::from("/tmp"),
Some("rust".to_string()),
Some(10),
Some(15),
vec![],
30,
5,
);
let files = vec![
PathBuf::from("nonexistent1.rs"),
PathBuf::from("nonexistent2.rs"),
];
let result = super::super::analyze_multiple_files(&files, &config).await;
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_file_complexity_metrics_structure() {
let metrics = FileComplexityMetrics {
path: "test.rs".to_string(),
total_complexity: ComplexityMetrics::new(15, 20, 5, 100),
functions: vec![FunctionComplexity {
name: "test_func".to_string(),
line_start: 1,
line_end: 10,
metrics: ComplexityMetrics::new(5, 8, 2, 10),
}],
classes: vec![],
};
assert_eq!(metrics.path, "test.rs");
assert_eq!(metrics.total_complexity.cyclomatic, 15);
assert_eq!(metrics.total_complexity.cognitive, 20);
assert_eq!(metrics.functions.len(), 1);
assert_eq!(metrics.functions[0].name, "test_func");
assert_eq!(metrics.functions[0].line_start, 1);
assert_eq!(metrics.functions[0].line_end, 10);
}
#[test]
fn test_function_complexity_structure() {
let func = FunctionComplexity {
name: "complex_function".to_string(),
line_start: 50,
line_end: 100,
metrics: ComplexityMetrics::new(25, 30, 8, 50),
};
assert_eq!(func.name, "complex_function");
assert_eq!(func.line_start, 50);
assert_eq!(func.line_end, 100);
assert_eq!(func.metrics.cyclomatic, 25);
assert_eq!(func.metrics.cognitive, 30);
assert_eq!(func.metrics.nesting_max, 8);
assert_eq!(func.metrics.lines, 50);
}
#[test]
fn test_complexity_metrics_new() {
let metrics = ComplexityMetrics::new(10, 15, 3, 25);
assert_eq!(metrics.cyclomatic, 10);
assert_eq!(metrics.cognitive, 15);
assert_eq!(metrics.nesting_max, 3);
assert_eq!(metrics.lines, 25);
}
#[test]
fn test_complexity_threshold_filtering_edge_cases() {
let file_at_threshold = FileComplexityMetrics {
path: "at_threshold.rs".to_string(),
total_complexity: ComplexityMetrics::new(10, 10, 3, 50),
functions: vec![FunctionComplexity {
name: "threshold_func".to_string(),
line_start: 1,
line_end: 20,
metrics: ComplexityMetrics::new(10, 10, 2, 20),
}],
classes: vec![],
};
let threshold = 10;
let should_include = file_at_threshold
.functions
.iter()
.any(|func| func.metrics.cyclomatic > threshold);
assert!(!should_include); }
#[test]
fn test_complexity_threshold_filtering_just_above() {
let file_above_threshold = FileComplexityMetrics {
path: "above_threshold.rs".to_string(),
total_complexity: ComplexityMetrics::new(11, 11, 3, 50),
functions: vec![FunctionComplexity {
name: "above_threshold_func".to_string(),
line_start: 1,
line_end: 20,
metrics: ComplexityMetrics::new(11, 11, 2, 20),
}],
classes: vec![],
};
let threshold = 10;
let should_include = file_above_threshold
.functions
.iter()
.any(|func| func.metrics.cyclomatic > threshold);
assert!(should_include); }
#[test]
fn test_multiple_functions_mixed_complexity() {
let file_mixed = FileComplexityMetrics {
path: "mixed.rs".to_string(),
total_complexity: ComplexityMetrics::new(20, 25, 5, 100),
functions: vec![
FunctionComplexity {
name: "simple_func".to_string(),
line_start: 1,
line_end: 10,
metrics: ComplexityMetrics::new(3, 5, 1, 10),
},
FunctionComplexity {
name: "complex_func".to_string(),
line_start: 20,
line_end: 50,
metrics: ComplexityMetrics::new(15, 20, 4, 30),
},
FunctionComplexity {
name: "very_complex_func".to_string(),
line_start: 60,
line_end: 100,
metrics: ComplexityMetrics::new(25, 30, 6, 40),
},
],
classes: vec![],
};
let threshold = 10;
let above_threshold_count = file_mixed
.functions
.iter()
.filter(|func| func.metrics.cyclomatic > threshold)
.count();
assert_eq!(above_threshold_count, 2);
let should_include = file_mixed
.functions
.iter()
.any(|func| func.metrics.cyclomatic > threshold);
assert!(should_include);
}
#[test]
fn test_fail_on_violation_boundary_exact() {
use super::super::has_complexity_violations;
let file = FileComplexityMetrics {
path: "test.rs".to_string(),
total_complexity: ComplexityMetrics::new(9, 9, 3, 100),
functions: vec![FunctionComplexity {
name: "func_with_9".to_string(),
line_start: 1,
line_end: 10,
metrics: ComplexityMetrics::new(9, 9, 3, 10),
}],
classes: vec![],
};
let has_violation = has_complexity_violations(std::slice::from_ref(&file), Some(10), None);
assert!(
!has_violation,
"Complexity 9 should NOT violate threshold 10 (9 is not > 10)"
);
let file_at_threshold = FileComplexityMetrics {
path: "test2.rs".to_string(),
total_complexity: ComplexityMetrics::new(10, 10, 3, 100),
functions: vec![FunctionComplexity {
name: "func_with_10".to_string(),
line_start: 1,
line_end: 10,
metrics: ComplexityMetrics::new(10, 10, 3, 10),
}],
classes: vec![],
};
let has_violation_at_threshold =
has_complexity_violations(&[file_at_threshold], Some(10), None);
assert!(
!has_violation_at_threshold,
"Complexity 10 should NOT violate threshold 10 (10 is not > 10)"
);
let file_above = FileComplexityMetrics {
path: "test3.rs".to_string(),
total_complexity: ComplexityMetrics::new(11, 11, 3, 100),
functions: vec![FunctionComplexity {
name: "func_with_11".to_string(),
line_start: 1,
line_end: 10,
metrics: ComplexityMetrics::new(11, 11, 3, 10),
}],
classes: vec![],
};
let has_violation_above = has_complexity_violations(&[file_above], Some(10), None);
assert!(
has_violation_above,
"Complexity 11 SHOULD violate threshold 10 (11 > 10)"
);
}
}