use hotspots_core::{analyze, analyze_with_progress, render_json, AnalysisOptions};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("tests")
.join("fixtures")
.join(name)
}
#[test]
fn test_simple_function() {
let path = fixture_path("simple.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).unwrap();
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].function, "simple");
}
#[test]
fn test_nested_branching() {
let path = fixture_path("nested-branching.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).unwrap();
assert_eq!(reports.len(), 1);
assert!(
reports[0].metrics.cc > 1,
"Nested branching should increase CC"
);
assert!(
reports[0].metrics.nd >= 2,
"Should have nesting depth of at least 2"
);
}
#[test]
fn test_loop_with_breaks() {
let path = fixture_path("loop-breaks.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).unwrap();
assert_eq!(reports.len(), 1);
assert!(
reports[0].metrics.ns > 0,
"Breaks and continues should be counted"
);
}
#[test]
fn test_try_catch_finally() {
let path = fixture_path("try-catch-finally.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).unwrap();
assert_eq!(reports.len(), 1);
assert!(
reports[0].metrics.cc >= 2,
"Try/catch should contribute to CC"
);
assert!(
reports[0].metrics.nd >= 1,
"Try should increase nesting depth"
);
}
#[test]
fn test_pathological_complexity() {
let path = fixture_path("pathological.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).unwrap();
assert_eq!(reports.len(), 1);
assert!(
reports[0].metrics.cc > 5,
"Pathological function should have high CC"
);
assert!(
reports[0].lrs > 5.0,
"Pathological function should have high LRS"
);
}
#[test]
fn test_deterministic_output() {
let path = fixture_path("simple.ts");
let options1 = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let options2 = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports1 = analyze(&path, options1).unwrap();
let reports2 = analyze(&path, options2).unwrap();
let json1 = render_json(&reports1);
let json2 = render_json(&reports2);
assert_eq!(json1, json2, "Output should be byte-for-byte identical");
}
#[test]
fn test_angular_decorators_parse_and_analyze() {
let path = fixture_path("angular-component.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).expect("Angular decorated TypeScript should parse");
assert!(
!reports.is_empty(),
"Should discover functions in Angular component"
);
let get_user = reports.iter().find(|r| r.function == "getUser");
assert!(get_user.is_some(), "Should find getUser method");
assert!(
get_user.unwrap().metrics.cc >= 2,
"getUser with if/throw should have CC >= 2"
);
let decorator_fn = reports
.iter()
.find(|r| r.function.contains("Component") || r.function.contains("Injectable"));
assert!(
decorator_fn.is_none(),
"Decorator expressions must not be counted as functions"
);
}
#[test]
fn test_react_jsx_in_plain_js_file() {
let path = fixture_path("react-component.js");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).expect("JSX in .js file should parse");
assert!(
!reports.is_empty(),
"Should discover functions in .js React component"
);
let counter = reports.iter().find(|r| r.function == "Counter");
assert!(counter.is_some(), "Should find Counter function");
let has_branching_fn = reports.iter().any(|r| r.metrics.cc >= 2);
assert!(
has_branching_fn,
"At least one function should have CC >= 2 from if branches"
);
let counter_cc = counter.unwrap().metrics.cc;
assert!(
counter_cc < 10,
"JSX elements must not inflate CC; got CC={}",
counter_cc
);
}
#[test]
fn test_lexer_panic_is_skipped_not_fatal() {
let path = fixture_path("lexer-panic.tsx");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options).unwrap();
assert_eq!(reports.len(), 0);
}
#[test]
fn test_progress_callback_sequence_single_file() {
let calls: Arc<Mutex<Vec<(usize, usize)>>> = Arc::new(Mutex::new(Vec::new()));
let calls_ref = calls.clone();
let path = fixture_path("simple.ts");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
analyze_with_progress(
&path,
options,
None,
Some(&move |done: usize, total: usize| {
calls_ref.lock().unwrap().push((done, total));
}),
)
.unwrap();
let recorded = calls.lock().unwrap();
assert_eq!(*recorded, vec![(0, 1), (1, 1)]);
}
#[test]
fn test_progress_callback_sequence_directory() {
let calls: Arc<Mutex<Vec<(usize, usize)>>> = Arc::new(Mutex::new(Vec::new()));
let calls_ref = calls.clone();
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("tests")
.join("fixtures")
.join("rust");
let options = AnalysisOptions {
min_lrs: None,
top_n: None,
};
analyze_with_progress(
&path,
options,
None,
Some(&move |done: usize, total: usize| {
calls_ref.lock().unwrap().push((done, total));
}),
)
.unwrap();
let recorded = calls.lock().unwrap();
assert!(
!recorded.is_empty(),
"progress must be called for non-empty dir"
);
let total = recorded[0].1;
assert!(total > 1, "rust fixtures dir should have multiple files");
assert_eq!(recorded[0], (0, total));
assert_eq!(
recorded.len(),
total + 1,
"expected 1 discovery + N per-file calls"
);
for &(_, t) in recorded.iter().skip(1) {
assert_eq!(t, total);
}
let mut done_values: Vec<usize> = recorded.iter().skip(1).map(|&(done, _)| done).collect();
done_values.sort_unstable();
assert_eq!(done_values, (1..=total).collect::<Vec<_>>());
}
#[test]
fn test_whitespace_invariance() {
let path = fixture_path("simple.ts");
let options1 = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let options2 = AnalysisOptions {
min_lrs: None,
top_n: None,
};
let reports = analyze(&path, options1).unwrap();
let lrs1 = reports[0].lrs;
let cc1 = reports[0].metrics.cc;
let reports2 = analyze(&path, options2).unwrap();
let lrs2 = reports2[0].lrs;
let cc2 = reports2[0].metrics.cc;
assert_eq!(lrs1, lrs2);
assert_eq!(cc1, cc2);
}