use crate::adapters::analyzers::dry::dead_code::DeadCodeWarning;
use crate::adapters::analyzers::dry::DeclaredFunction;
use crate::adapters::analyzers::tq::untested::*;
use crate::adapters::analyzers::tq::{TqWarning, TqWarningKind};
use crate::config::Config;
use std::collections::{HashMap, HashSet, VecDeque};
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,
}
}
#[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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
assert!(
warnings.is_empty(),
"qual:api 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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &dead, &config);
assert!(warnings.is_empty());
}
#[test]
fn test_transitive_tested_not_flagged() {
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 test_calls: HashSet<String> = ["a".to_string()].into();
let call_graph: HashMap<String, Vec<String>> =
[("a".to_string(), vec!["b".to_string()])].into();
let tested = build_transitive_tested_set(&test_calls, &call_graph);
let config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
assert!(warnings.is_empty(), "b is transitively tested via a");
}
#[test]
fn test_deep_transitive_not_flagged() {
let declared = vec![
make_declared("a", false),
make_declared("b", false),
make_declared("c", false),
];
let prod_calls: HashSet<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();
let test_calls: HashSet<String> = ["a".to_string()].into();
let call_graph: HashMap<String, Vec<String>> = [
("a".to_string(), vec!["b".to_string()]),
("b".to_string(), vec!["c".to_string()]),
]
.into();
let tested = build_transitive_tested_set(&test_calls, &call_graph);
let config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
assert!(warnings.is_empty(), "c is transitively tested via a→b→c");
}
#[test]
fn test_circular_calls_no_infinite_loop() {
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 test_calls: HashSet<String> = ["a".to_string()].into();
let call_graph: HashMap<String, Vec<String>> = [
("a".to_string(), vec!["b".to_string()]),
("b".to_string(), vec!["a".to_string()]),
]
.into();
let tested = build_transitive_tested_set(&test_calls, &call_graph);
let config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
assert!(
warnings.is_empty(),
"cycle terminates; both a and b are tested"
);
}
#[test]
fn test_untested_leaf_still_flagged() {
let declared = vec![
make_declared("a", false),
make_declared("b", false),
make_declared("d", false),
];
let prod_calls: HashSet<String> = ["a", "b", "d"].iter().map(|s| s.to_string()).collect();
let test_calls: HashSet<String> = ["a".to_string()].into();
let call_graph: HashMap<String, Vec<String>> =
[("a".to_string(), vec!["b".to_string()])].into();
let tested = build_transitive_tested_set(&test_calls, &call_graph);
let config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
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 config = Config::default();
let warnings = detect_untested_functions(&declared, &prod_calls, &tested, &[], &config);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].function_name, "b");
}