use crate::adapters::analyzers::dry::dead_code::DeadCodeWarning;
use crate::adapters::analyzers::tq::build_reaches_prod_set;
use crate::adapters::analyzers::tq::untested::*;
use crate::adapters::analyzers::tq::{TqWarning, TqWarningKind};
use crate::adapters::shared::declared_function::DeclaredFunction;
use std::collections::{HashMap, HashSet};
fn make_declared(name: &str, is_test: bool) -> DeclaredFunction {
DeclaredFunction {
name: name.to_string(),
qualified_name: name.to_string(),
file: "lib.rs".to_string(),
line: 1,
is_test,
is_main: false,
is_trait_impl: false,
has_allow_dead_code: false,
is_api: false,
is_test_helper: false,
}
}
fn untested_via_graph(
declared: &[&str],
prod: &[&str],
test: &[&str],
edges: &[(&str, &str)],
) -> Vec<TqWarning> {
let declared: Vec<DeclaredFunction> =
declared.iter().map(|n| make_declared(n, false)).collect();
let prod_calls: HashSet<String> = prod.iter().map(|s| s.to_string()).collect();
let test_calls: HashSet<String> = test.iter().map(|s| s.to_string()).collect();
let call_graph: HashMap<String, Vec<String>> = edges
.iter()
.map(|(f, t)| (f.to_string(), vec![t.to_string()]))
.collect();
let tested = build_transitive_tested_set(&test_calls, &call_graph);
detect_untested_functions(&declared, &prod_calls, &tested, &[])
}
#[test]
fn test_untested_prod_fn_emits_warning() {
let declared = vec![make_declared("process", false)];
let prod_calls: HashSet<String> = ["process".to_string()].into();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].kind, TqWarningKind::Untested);
assert_eq!(warnings[0].function_name, "process");
}
#[test]
fn test_tested_fn_no_warning() {
let declared = vec![make_declared("process", false)];
let prod_calls: HashSet<String> = ["process".to_string()].into();
let tested: HashSet<String> = ["process".to_string()].into();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(warnings.is_empty());
}
#[test]
fn test_uncalled_fn_no_warning() {
let declared = vec![make_declared("unused", false)];
let prod_calls: HashSet<String> = HashSet::new();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(
warnings.is_empty(),
"functions not called from prod are not TQ-003"
);
}
#[test]
fn test_test_fn_excluded() {
let declared = vec![make_declared("test_helper", true)];
let prod_calls: HashSet<String> = ["test_helper".to_string()].into();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(warnings.is_empty());
}
#[test]
fn test_main_fn_excluded() {
let mut declared = vec![make_declared("main", false)];
declared[0].is_main = true;
let prod_calls: HashSet<String> = ["main".to_string()].into();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(warnings.is_empty());
}
#[test]
fn test_api_fn_excluded() {
let mut declared = vec![make_declared("handle_overview", false)];
declared[0].is_api = true;
let prod_calls: HashSet<String> = ["handle_overview".to_string()].into();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(
warnings.is_empty(),
"qual:api functions should be excluded from TQ-003"
);
}
#[test]
fn test_test_helper_fn_excluded() {
let mut declared = vec![make_declared("shared_asserter", false)];
declared[0].is_test_helper = true;
let prod_calls: HashSet<String> = ["shared_asserter".to_string()].into();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(
warnings.is_empty(),
"qual:test_helper functions should be excluded from TQ-003"
);
}
#[test]
fn test_trait_impl_excluded() {
let mut declared = vec![make_declared("fmt", false)];
declared[0].is_trait_impl = true;
let prod_calls: HashSet<String> = ["fmt".to_string()].into();
let tested = HashSet::new();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert!(warnings.is_empty());
}
#[test]
fn test_dead_code_excluded() {
let declared = vec![make_declared("dead_fn", false)];
let prod_calls: HashSet<String> = ["dead_fn".to_string()].into();
let tested = HashSet::new();
let dead = vec![
crate::adapters::analyzers::dry::dead_code::DeadCodeWarning {
function_name: "dead_fn".to_string(),
file: "lib.rs".to_string(),
line: 1,
kind: crate::adapters::analyzers::dry::dead_code::DeadCodeKind::Uncalled,
qualified_name: "dead_fn".to_string(),
suggestion: String::new(),
},
];
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &dead);
assert!(warnings.is_empty());
}
#[test]
fn test_transitive_tested_not_flagged() {
let warnings = untested_via_graph(&["a", "b"], &["a", "b"], &["a"], &[("a", "b")]);
assert!(warnings.is_empty(), "b is transitively tested via a");
}
#[test]
fn test_deep_transitive_not_flagged() {
let warnings = untested_via_graph(
&["a", "b", "c"],
&["a", "b", "c"],
&["a"],
&[("a", "b"), ("b", "c")],
);
assert!(warnings.is_empty(), "c is transitively tested via a→b→c");
}
#[test]
fn test_circular_calls_no_infinite_loop() {
let warnings = untested_via_graph(&["a", "b"], &["a", "b"], &["a"], &[("a", "b"), ("b", "a")]);
assert!(
warnings.is_empty(),
"cycle terminates; both a and b are tested"
);
}
#[test]
fn test_untested_leaf_still_flagged() {
let warnings = untested_via_graph(&["a", "b", "d"], &["a", "b", "d"], &["a"], &[("a", "b")]);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].function_name, "d");
}
#[test]
fn test_empty_call_graph_falls_back_to_direct() {
let declared = vec![make_declared("a", false), make_declared("b", false)];
let prod_calls: HashSet<String> = ["a", "b"].iter().map(|s| s.to_string()).collect();
let tested: HashSet<String> = ["a".to_string()].into();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[]);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].function_name, "b");
}
#[test]
fn build_reaches_prod_set_seeds_prod_and_walks_callers_backward() {
let mut call_graph: HashMap<String, Vec<String>> = HashMap::new();
call_graph.insert("helper".to_string(), vec!["prod_fn".to_string()]);
call_graph.insert("test_a".to_string(), vec!["helper".to_string()]);
let declared = vec![
make_declared("prod_fn", false),
make_declared("lonely_test", true),
];
let reaches = build_reaches_prod_set(&call_graph, &declared);
assert!(reaches.contains("prod_fn"), "production fn seeds the set");
assert!(reaches.contains("helper"), "helper calls prod_fn");
assert!(
reaches.contains("test_a"),
"test_a transitively reaches prod via helper"
);
assert!(
!reaches.contains("lonely_test"),
"a test fn reaching no prod code is excluded from the seed"
);
}