use heal_cli::core::config::Config;
#[cfg(feature = "lang-rust")]
use heal_cli::observer::code::complexity::ComplexityMetric;
use heal_cli::observer::code::complexity::ComplexityObserver;
mod common;
#[allow(unused_imports)]
use common::write;
#[allow(dead_code)]
fn enabled_observer() -> ComplexityObserver {
ComplexityObserver {
excluded: Vec::new(),
ccn_enabled: true,
cognitive_enabled: true,
workspace: None,
}
}
#[test]
fn returns_empty_report_when_both_metrics_disabled() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "src/lib.rs", "fn one() { if true {} }\n");
let observer = ComplexityObserver {
excluded: Vec::new(),
ccn_enabled: false,
cognitive_enabled: false,
workspace: None,
};
let report = observer.scan(dir.path());
assert!(report.files.is_empty());
assert_eq!(report.totals.functions, 0);
}
#[cfg(all(feature = "lang-typescript", feature = "lang-rust"))]
#[test]
fn aggregates_metrics_across_typescript_and_rust() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"src/outer.rs",
"fn outer(a: bool) -> i32 { if a { 1 } else { 0 } }\n",
);
write(
dir.path(),
"src/app.ts",
"function app(a: number, b: number) { if (a > 0) {} if (b > 0) {} }\n",
);
let report = enabled_observer().scan(dir.path());
assert_eq!(report.files.len(), 2, "got files: {:?}", report.files);
let app = &report.files[0];
assert_eq!(app.language, "typescript");
assert_eq!(app.functions.len(), 1);
assert_eq!(app.functions[0].ccn, 3);
let outer = &report.files[1];
assert_eq!(outer.language, "rust");
assert_eq!(outer.functions.len(), 1);
assert_eq!(outer.functions[0].ccn, 2);
assert_eq!(outer.functions[0].cognitive, 2);
assert_eq!(report.totals.files, 2);
assert_eq!(report.totals.functions, 2);
assert_eq!(report.totals.max_ccn, 3);
}
#[cfg(feature = "lang-rust")]
#[test]
fn excluded_substrings_skip_files() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "src/keep.rs", "fn k() {}\n");
write(dir.path(), "vendor/skip.rs", "fn s() { if true {} }\n");
let observer = ComplexityObserver {
excluded: vec!["vendor".to_string()],
ccn_enabled: true,
cognitive_enabled: true,
workspace: None,
};
let report = observer.scan(dir.path());
assert_eq!(report.files.len(), 1);
assert!(report.files[0].path.ends_with("keep.rs"));
}
#[cfg(feature = "lang-rust")]
#[test]
fn gitignore_is_respected() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), ".gitignore", "target/\n");
write(dir.path(), "src/lib.rs", "fn a() {}\n");
write(
dir.path(),
"target/debug.rs",
"fn ignored() { if true {} }\n",
);
let report = enabled_observer().scan(dir.path());
let paths: Vec<String> = report
.files
.iter()
.map(|f| f.path.to_string_lossy().into_owned())
.collect();
assert!(
paths.iter().any(|p| p.ends_with("lib.rs")),
"expected lib.rs in {paths:?}"
);
assert!(
paths.iter().all(|p| !p.contains("target")),
"target/ should be gitignored, got {paths:?}"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn worst_n_orders_descending_by_metric() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"src/lo.rs",
"fn lo() { if true {} }\n", );
write(
dir.path(),
"src/mid.rs",
"fn mid() { if true {} if true {} }\n", );
write(
dir.path(),
"src/hi.rs",
"fn hi() { if true {} if true {} if true {} }\n", );
let report = enabled_observer().scan(dir.path());
let top2 = report.worst_n(2, ComplexityMetric::Ccn);
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].name, "hi", "got {top2:?}");
assert_eq!(top2[0].ccn, 4);
assert_eq!(top2[1].name, "mid");
assert_eq!(top2[1].ccn, 3);
let top1_cog = report.worst_n(1, ComplexityMetric::Cognitive);
assert_eq!(top1_cog.len(), 1);
assert_eq!(top1_cog[0].name, "hi");
assert_eq!(top1_cog[0].cognitive, 3);
}
#[test]
fn from_config_inherits_excludes_and_toggles() {
let mut cfg = Config::default();
cfg.git.exclude_paths = vec!["dist".to_string()];
cfg.metrics.loc.exclude_paths = vec!["vendor".to_string()];
let observer = ComplexityObserver::from_config(&cfg);
assert_eq!(
observer.excluded,
vec!["dist".to_string(), "vendor".to_string()]
);
assert!(observer.ccn_enabled);
assert!(observer.cognitive_enabled);
}
#[test]
fn into_findings_disambiguates_same_name_same_span_functions() {
use heal_cli::core::finding::IntoFindings;
use heal_cli::observer::code::complexity::{
ComplexityReport, ComplexityTotals, FileComplexity, FunctionMetric,
};
let fun = |start: u32| FunctionMetric {
name: "new".to_owned(),
start_line: start,
end_line: start,
ccn: 2,
cognitive: 1,
};
let report = ComplexityReport {
files: vec![FileComplexity {
path: "src/foo.rs".into(),
language: "rust".to_owned(),
functions: vec![fun(10), fun(50)],
}],
totals: ComplexityTotals {
files: 1,
functions: 2,
max_ccn: 2,
max_cognitive: 1,
},
};
let findings = report.into_findings();
assert_eq!(findings.len(), 4);
let ids = |metric: &str| -> Vec<&str> {
findings
.iter()
.filter(|f| f.metric == metric)
.map(|f| f.id.as_str())
.collect()
};
let ccn_ids = ids("ccn");
assert_eq!(ccn_ids.len(), 2);
assert_ne!(ccn_ids[0], ccn_ids[1]);
let cog_ids = ids("cognitive");
assert_ne!(cog_ids[0], cog_ids[1]);
let again = report.into_findings();
let all = |fs: &[heal_cli::core::finding::Finding]| -> Vec<String> {
fs.iter().map(|f| f.id.clone()).collect()
};
assert_eq!(all(&findings), all(&again));
}