use std::fs;
use std::path::PathBuf;
use crate::complexity::{build_complexity_report, generate_complexity_histogram};
use tokmd_analysis_types::AnalysisLimits;
use tokmd_analysis_types::{ComplexityRisk, FileComplexity};
use tokmd_types::{ChildIncludeMode, ExportData, FileKind, FileRow};
fn make_row(path: &str, lang: &str, code: usize) -> FileRow {
FileRow {
path: path.to_string(),
module: "src".to_string(),
lang: lang.to_string(),
kind: FileKind::Parent,
code,
comments: 0,
blanks: 0,
lines: code,
bytes: code * 40,
tokens: code * 8,
}
}
fn make_export(rows: Vec<FileRow>) -> ExportData {
ExportData {
rows,
module_roots: vec![],
module_depth: 1,
children: ChildIncludeMode::Separate,
}
}
fn write_temp_files(files: &[(&str, &str)]) -> (tempfile::TempDir, Vec<PathBuf>) {
let dir = tempfile::tempdir().expect("create tempdir");
let mut paths = Vec::new();
for (rel, content) in files {
let full = dir.path().join(rel);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&full, content).unwrap();
paths.push(PathBuf::from(rel));
}
(dir, paths)
}
fn analyze(files: &[(&str, &str, &str)], detail: bool) -> tokmd_analysis_types::ComplexityReport {
let file_entries: Vec<(&str, &str)> = files.iter().map(|(p, _, c)| (*p, *c)).collect();
let (dir, paths) = write_temp_files(&file_entries);
let rows: Vec<FileRow> = files
.iter()
.map(|(p, lang, c)| make_row(p, lang, c.lines().count()))
.collect();
let export = make_export(rows);
build_complexity_report(
dir.path(),
&paths,
&export,
&AnalysisLimits::default(),
detail,
)
.unwrap()
}
mod cyclomatic_patterns {
use super::*;
#[test]
fn while_loop_adds_one() {
let code = "fn run() {\n while true {\n break;\n }\n}\n";
let r = analyze(&[("w.rs", "Rust", code)], false);
assert_eq!(r.files[0].cyclomatic_complexity, 2);
}
#[test]
fn loop_keyword_adds_one() {
let code = "fn run() {\n loop {\n break;\n }\n}\n";
let r = analyze(&[("l.rs", "Rust", code)], false);
assert_eq!(r.files[0].cyclomatic_complexity, 2);
}
#[test]
fn logical_and_or_adds_complexity() {
let code = "fn check(a: bool, b: bool, c: bool) -> bool {\n a && b || c\n}\n";
let r = analyze(&[("logic.rs", "Rust", code)], false);
assert_eq!(r.files[0].cyclomatic_complexity, 3);
}
#[test]
fn question_mark_operator_adds_one() {
let code = "fn try_it() -> Result<(), ()> {\n let x = ok()?;\n Ok(())\n}\n";
let r = analyze(&[("q.rs", "Rust", code)], false);
assert_eq!(r.files[0].cyclomatic_complexity, 2);
}
#[test]
fn deeply_nested_ifs_accumulate() {
let code = r#"fn deep(x: i32) {
if x > 0 {
if x > 10 {
if x > 100 {
if x > 1000 {
println!("huge");
}
}
}
}
}
"#;
let r = analyze(&[("deep.rs", "Rust", code)], false);
assert_eq!(r.files[0].cyclomatic_complexity, 5);
}
#[test]
fn python_for_while_if() {
let code = "def f(items):\n for item in items:\n while item > 0:\n if item % 2 == 0:\n item -= 1\n";
let r = analyze(&[("f.py", "Python", code)], false);
assert_eq!(r.files[0].cyclomatic_complexity, 4);
}
#[test]
fn go_select_case() {
let code = r#"func main() {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
"#;
let r = analyze(&[("main.go", "Go", code)], false);
assert!(r.files[0].cyclomatic_complexity >= 2);
}
#[test]
fn javascript_ternary_adds_one() {
let code = "function f(x) {\n return x > 0 ? 'pos' : 'neg';\n}\n";
let r = analyze(&[("f.js", "JavaScript", code)], false);
assert!(
r.files[0].cyclomatic_complexity >= 2,
"ternary should increase complexity"
);
}
}
mod cognitive_scoring {
use super::*;
#[test]
fn linear_code_low_cognitive() {
let code = "fn linear() {\n let a = 1;\n let b = 2;\n let c = a + b;\n}\n";
let r = analyze(&[("lin.rs", "Rust", code)], false);
if let Some(cog) = r.files[0].cognitive_complexity {
assert!(cog <= 1, "linear code → cognitive ≤ 1, got {cog}");
}
}
#[test]
fn nested_code_higher_cognitive_than_flat() {
let flat = r#"fn flat(x: i32) {
if x > 0 { println!("a"); }
if x > 10 { println!("b"); }
if x > 100 { println!("c"); }
}
"#;
let nested = r#"fn nested(x: i32) {
if x > 0 {
if x > 10 {
if x > 100 {
println!("deep");
}
}
}
}
"#;
let r_flat = analyze(&[("flat.rs", "Rust", flat)], false);
let r_nested = analyze(&[("nested.rs", "Rust", nested)], false);
let cog_flat = r_flat.files[0].cognitive_complexity.unwrap_or(0);
let cog_nested = r_nested.files[0].cognitive_complexity.unwrap_or(0);
assert!(
cog_nested >= cog_flat,
"nested ({cog_nested}) should be >= flat ({cog_flat})"
);
}
}
mod risk_thresholds {
use super::*;
#[test]
fn simple_code_is_low_risk() {
let code = "fn simple() {\n let x = 1;\n}\n";
let r = analyze(&[("simple.rs", "Rust", code)], false);
assert_eq!(
r.files[0].risk_level,
ComplexityRisk::Low,
"trivial code → low risk"
);
}
#[test]
fn high_cyclomatic_elevates_risk() {
let mut code = String::from("fn complex(x: i32) {\n");
for i in 0..30 {
code.push_str(&format!(" if x > {i} {{ println!(\"{i}\"); }}\n"));
}
code.push_str("}\n");
let r = analyze(&[("complex.rs", "Rust", &code)], false);
assert!(
r.files[0].cyclomatic_complexity > 20,
"should have high cyclomatic"
);
assert!(
r.files[0].risk_level != ComplexityRisk::Low,
"high cyclomatic → not low risk"
);
}
#[test]
fn high_risk_files_count_matches() {
let simple = "fn a() {}\n";
let complex_code = {
let mut s = String::from("fn b(x: i32) {\n");
for i in 0..60 {
s.push_str(&format!(" if x > {i} {{ println!(\"{i}\"); }}\n"));
}
s.push_str("}\n");
s
};
let r = analyze(
&[
("simple.rs", "Rust", simple),
("complex.rs", "Rust", &complex_code),
],
false,
);
let actual_high = r
.files
.iter()
.filter(|f| {
matches!(
f.risk_level,
ComplexityRisk::High | ComplexityRisk::Critical
)
})
.count();
assert_eq!(r.high_risk_files, actual_high);
}
}
mod sorting {
use super::*;
#[test]
fn files_sorted_by_cyclomatic_descending() {
let low = "fn a() { let x = 1; }\n";
let mid = "fn b(x: i32) {\n if x > 0 { println!(\"a\"); }\n if x > 1 { println!(\"b\"); }\n}\n";
let high = {
let mut s = String::from("fn c(x: i32) {\n");
for i in 0..10 {
s.push_str(&format!(" if x > {i} {{ println!(\"{i}\"); }}\n"));
}
s.push_str("}\n");
s
};
let r = analyze(
&[
("low.rs", "Rust", low),
("high.rs", "Rust", &high),
("mid.rs", "Rust", mid),
],
false,
);
for w in r.files.windows(2) {
assert!(
w[0].cyclomatic_complexity >= w[1].cyclomatic_complexity,
"files not sorted descending: {} < {}",
w[0].cyclomatic_complexity,
w[1].cyclomatic_complexity
);
}
}
#[test]
fn tie_breaking_is_deterministic() {
let code = "fn f() { let x = 1; }\n";
let r1 = analyze(
&[
("a.rs", "Rust", code),
("b.rs", "Rust", code),
("c.rs", "Rust", code),
],
false,
);
let r2 = analyze(
&[
("c.rs", "Rust", code),
("a.rs", "Rust", code),
("b.rs", "Rust", code),
],
false,
);
let paths1: Vec<&str> = r1.files.iter().map(|f| f.path.as_str()).collect();
let paths2: Vec<&str> = r2.files.iter().map(|f| f.path.as_str()).collect();
assert_eq!(paths1, paths2, "same complexity → deterministic path order");
}
}
mod histogram_round2 {
use super::*;
#[test]
fn all_low_complexity_in_first_bucket() {
let files: Vec<FileComplexity> = (0..5)
.map(|i| FileComplexity {
path: format!("f{i}.rs"),
module: "src".to_string(),
function_count: 1,
max_function_length: 5,
cyclomatic_complexity: 2,
cognitive_complexity: None,
max_nesting: None,
risk_level: ComplexityRisk::Low,
functions: None,
})
.collect();
let h = generate_complexity_histogram(&files, 5);
assert_eq!(h.counts[0], 5, "all low complexity in first bucket");
assert_eq!(h.total, 5);
}
#[test]
fn high_complexity_clamped_to_last_bucket() {
let files = vec![FileComplexity {
path: "huge.rs".to_string(),
module: "src".to_string(),
function_count: 100,
max_function_length: 500,
cyclomatic_complexity: 999,
cognitive_complexity: Some(500),
max_nesting: Some(15),
risk_level: ComplexityRisk::Critical,
functions: None,
}];
let h = generate_complexity_histogram(&files, 5);
assert_eq!(
h.counts.last().copied().unwrap_or(0),
1,
"huge complexity in last bucket"
);
}
}