#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_basic_complexity() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
r#"
fn simple() -> i32 {
42
}
fn with_if(x: i32) -> i32 {
if x > 0 {
x
} else {
-x
}
}
"#,
)
.unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.functions.len(), 2);
assert_eq!(result.functions[0].cyclomatic_complexity, 1);
assert_eq!(result.functions[1].cyclomatic_complexity, 2);
}
#[tokio::test]
async fn test_line_numbers_are_accurate() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
"fn first() -> i32 {\n 42\n}\n\nfn second() -> i32 {\n 99\n}\n",
)
.unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.functions.len(), 2);
assert_eq!(result.functions[0].name, "first");
assert_eq!(result.functions[0].line_start, 1);
assert_eq!(result.functions[1].name, "second");
assert_eq!(result.functions[1].line_start, 5);
}
#[tokio::test]
async fn test_line_end_and_total_lines_are_measured() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
"fn first() -> i32 {\n 42\n}\n\nfn second() -> i32 {\n 99\n}\n",
)
.unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.total_lines, 7, "file is 7 lines long");
assert_eq!(result.functions[0].line_end, 3);
assert_eq!(result.functions[1].line_end, 7);
for func in &result.functions {
assert!(
func.line_end <= result.total_lines,
"{} ends at {} but the file has {} lines",
func.name,
func.line_end,
result.total_lines
);
}
}
#[tokio::test]
async fn test_single_line_file_does_not_report_51_lines() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("one.rs");
fs::write(&test_file, "fn only_one() -> i32 { 42 }\n").unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.total_lines, 1);
assert_eq!(result.functions.len(), 1);
assert_eq!(result.functions[0].line_start, 1);
assert_eq!(result.functions[0].line_end, 1);
}
#[tokio::test]
async fn test_max_nesting_is_measured() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("nest.rs");
fs::write(
&test_file,
"fn deep(a: i32) -> i32 {\n if a > 0 {\n if a > 1 {\n if a > 2 {\n return 3;\n }\n }\n }\n 0\n}\n",
)
.unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.functions[0].max_nesting, 3);
}
#[tokio::test]
async fn test_line_numbers_with_attributes() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
"#[inline]\npub fn decorated() -> i32 {\n 42\n}\n\n/// doc comment\npub async fn async_fn() {}\n",
)
.unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.functions.len(), 2);
assert_eq!(result.functions[0].name, "decorated");
assert_eq!(result.functions[0].line_start, 2);
assert_eq!(result.functions[1].name, "async_fn");
assert_eq!(result.functions[1].line_start, 7);
}
#[test]
fn test_build_function_line_map() {
let content = "fn foo() {}\n\npub fn bar() {}\n\nasync fn baz() {}\n";
let map = build_function_line_map(content);
assert_eq!(map.peek("foo"), Some(LineSpan { start: 1, end: 1 }));
assert_eq!(map.peek("bar"), Some(LineSpan { start: 3, end: 3 }));
assert_eq!(map.peek("baz"), Some(LineSpan { start: 5, end: 5 }));
assert_eq!(map.total_lines(), 5);
}
#[test]
fn test_build_function_line_map_skips_comments() {
let content = "// fn not_a_function() {}\nfn real() {}\n";
let map = build_function_line_map(content);
assert_eq!(map.peek("real"), Some(LineSpan { start: 2, end: 2 }));
assert_eq!(map.peek("not_a_function"), None);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_analyzer_new_defaults() {
let analyzer = AccurateComplexityAnalyzer::new();
assert!(!analyzer.exclude_tests);
assert!(!analyzer.respect_annotations);
}
#[test]
fn test_analyzer_builder_exclude_tests() {
let analyzer = AccurateComplexityAnalyzer::new().exclude_tests(true);
assert!(analyzer.exclude_tests);
}
#[test]
fn test_analyzer_builder_respect_annotations() {
let analyzer = AccurateComplexityAnalyzer::new().respect_annotations(true);
assert!(analyzer.respect_annotations);
}
#[test]
fn test_analyzer_builder_chaining() {
let analyzer = AccurateComplexityAnalyzer::new()
.exclude_tests(true)
.respect_annotations(true);
assert!(analyzer.exclude_tests);
assert!(analyzer.respect_annotations);
}
#[test]
fn test_is_test_file_test_suffix() {
let analyzer = AccurateComplexityAnalyzer::new().exclude_tests(true);
let path = Path::new("src/foo_test.rs");
assert!(analyzer.is_test_file(path));
}
#[test]
fn test_is_test_file_tests_suffix() {
let analyzer = AccurateComplexityAnalyzer::new().exclude_tests(true);
let path = Path::new("src/foo_tests.rs");
assert!(analyzer.is_test_file(path));
}
#[test]
fn test_is_test_file_tests_dir() {
let analyzer = AccurateComplexityAnalyzer::new().exclude_tests(true);
let path = Path::new("src/tests/integration.rs");
assert!(analyzer.is_test_file(path));
}
#[test]
fn test_is_test_file_regular_file() {
let analyzer = AccurateComplexityAnalyzer::new().exclude_tests(true);
let path = Path::new("src/lib.rs");
assert!(!analyzer.is_test_file(path));
}
#[test]
fn test_is_test_file_always_checks() {
let analyzer = AccurateComplexityAnalyzer::new().exclude_tests(false);
let path = Path::new("src/foo_test.rs");
assert!(analyzer.is_test_file(path));
}
#[test]
fn test_default_impl() {
let analyzer = AccurateComplexityAnalyzer::default();
assert!(!analyzer.exclude_tests);
assert!(!analyzer.respect_annotations);
}
fn parse_fn(src: &str) -> ItemFn {
syn::parse_str::<ItemFn>(src).expect("parse fn")
}
#[test]
fn test_analyze_function_suppressed_when_both_true() {
let func = parse_fn("#[allow(complex_function)] fn f() { if true {} else {} }");
let analyzer = AccurateComplexityAnalyzer::new().respect_annotations(true);
let metrics = analyzer.analyze_function(&func, LineSpan { start: 1, end: 1 });
assert_eq!(metrics.name, "f");
assert!(metrics.suppressed, "both sides true: must be suppressed");
}
#[test]
fn test_analyze_function_not_suppressed_when_only_flag_true() {
let func = parse_fn("fn f() { if true {} else {} }");
let analyzer = AccurateComplexityAnalyzer::new().respect_annotations(true);
let metrics = analyzer.analyze_function(&func, LineSpan { start: 1, end: 1 });
assert!(
!metrics.suppressed,
"respect_annotations alone must not suppress"
);
}
#[test]
fn test_analyze_function_not_suppressed_when_only_attr_true() {
let func = parse_fn("#[allow(complex_function)] fn f() { if true {} else {} }");
let analyzer = AccurateComplexityAnalyzer::new(); let metrics = analyzer.analyze_function(&func, LineSpan { start: 1, end: 1 });
assert!(
!metrics.suppressed,
"attr alone must not suppress without respect_annotations=true"
);
}
#[test]
fn test_analyze_function_not_suppressed_when_both_false() {
let func = parse_fn("fn f() {}");
let analyzer = AccurateComplexityAnalyzer::new();
let metrics = analyzer.analyze_function(&func, LineSpan { start: 42, end: 44 });
assert!(!metrics.suppressed);
assert_eq!(metrics.line_start, 42, "line_start must be propagated");
}
#[test]
fn test_has_suppress_annotation_non_allow_attr() {
let func = parse_fn("#[inline] fn f() {}");
let analyzer = AccurateComplexityAnalyzer::new();
assert!(!analyzer.has_suppress_annotation(&func.attrs));
}
#[test]
fn test_has_suppress_annotation_allow_without_complex_function() {
let func = parse_fn("#[allow(dead_code)] fn f() {}");
let analyzer = AccurateComplexityAnalyzer::new();
assert!(!analyzer.has_suppress_annotation(&func.attrs));
}
#[test]
fn test_has_suppress_annotation_allow_complex_function() {
let func = parse_fn("#[allow(complex_function)] fn f() {}");
let analyzer = AccurateComplexityAnalyzer::new();
assert!(analyzer.has_suppress_annotation(&func.attrs));
}
#[test]
fn test_has_suppress_annotation_mixed_multi_attr() {
let func = parse_fn("#[inline] #[allow(complex_function)] fn f() {}");
let analyzer = AccurateComplexityAnalyzer::new();
assert!(analyzer.has_suppress_annotation(&func.attrs));
}
#[test]
fn test_has_suppress_annotation_allow_path_no_list() {
let func = parse_fn(r#"#[allow = "whatever"] fn f() {}"#);
let analyzer = AccurateComplexityAnalyzer::new();
assert!(!analyzer.has_suppress_annotation(&func.attrs));
}
}