use super::*;
use std::path::{Path, PathBuf};
#[test]
fn test_call_graph_basic() {
let mut graph = CallGraph::new();
let (main_id, helper_id) = create_test_functions();
add_functions_to_graph(&mut graph, &main_id, &helper_id);
add_call_edge(&mut graph, &main_id, &helper_id);
verify_basic_graph_properties(&graph, &main_id, &helper_id);
}
fn create_test_functions() -> (FunctionId, FunctionId) {
let main_id = FunctionId::new(PathBuf::from("main.rs"), "main".to_string(), 1);
let helper_id = FunctionId::new(PathBuf::from("lib.rs"), "helper".to_string(), 10);
(main_id, helper_id)
}
fn add_functions_to_graph(graph: &mut CallGraph, main_id: &FunctionId, helper_id: &FunctionId) {
graph.add_function(main_id.clone(), true, false, 2, 20);
graph.add_function(helper_id.clone(), false, false, 5, 30);
}
fn add_call_edge(graph: &mut CallGraph, caller: &FunctionId, callee: &FunctionId) {
graph.add_call(FunctionCall {
caller: caller.clone(),
callee: callee.clone(),
call_type: CallType::Direct,
});
}
fn verify_basic_graph_properties(graph: &CallGraph, main_id: &FunctionId, helper_id: &FunctionId) {
assert_eq!(graph.get_callees(main_id).len(), 1);
assert_eq!(graph.get_callers(helper_id).len(), 1);
assert!(graph.is_entry_point(main_id));
assert!(!graph.is_entry_point(helper_id));
}
#[test]
fn test_transitive_dependencies() {
let mut graph = CallGraph::new();
let (a, b, c) = create_chain_functions();
add_chain_to_graph(&mut graph, &a, &b, &c);
verify_transitive_callees(&graph, &a, &b, &c);
}
fn create_chain_functions() -> (FunctionId, FunctionId, FunctionId) {
let a = FunctionId::new(PathBuf::from("a.rs"), "a".to_string(), 1);
let b = FunctionId::new(PathBuf::from("b.rs"), "b".to_string(), 1);
let c = FunctionId::new(PathBuf::from("c.rs"), "c".to_string(), 1);
(a, b, c)
}
fn add_chain_to_graph(graph: &mut CallGraph, a: &FunctionId, b: &FunctionId, c: &FunctionId) {
graph.add_function(a.clone(), true, false, 1, 10);
graph.add_function(b.clone(), false, false, 2, 20);
graph.add_function(c.clone(), false, false, 3, 30);
add_call_edge(graph, a, b);
add_call_edge(graph, b, c);
}
fn verify_transitive_callees(graph: &CallGraph, a: &FunctionId, b: &FunctionId, c: &FunctionId) {
let transitive = graph.get_transitive_callees(a, 3);
assert_eq!(transitive.len(), 2);
assert!(transitive.contains(b));
assert!(transitive.contains(c));
}
#[test]
fn test_find_function_at_location() {
let mut graph = CallGraph::new();
let file = PathBuf::from("test.rs");
let functions = create_located_functions(&file);
add_located_functions(&mut graph, &functions);
verify_function_location_finding(&graph, &file, &functions);
}
fn create_located_functions(file: &Path) -> Vec<FunctionId> {
vec![
FunctionId::new(file.to_path_buf(), "function_one".to_string(), 10),
FunctionId::new(file.to_path_buf(), "function_two".to_string(), 30),
FunctionId::new(file.to_path_buf(), "function_three".to_string(), 50),
]
}
fn add_located_functions(graph: &mut CallGraph, functions: &[FunctionId]) {
graph.add_function(functions[0].clone(), false, false, 5, 15);
graph.add_function(functions[1].clone(), false, false, 3, 10);
graph.add_function(functions[2].clone(), false, false, 4, 20);
}
fn verify_function_location_finding(graph: &CallGraph, file: &PathBuf, _functions: &[FunctionId]) {
assert_eq!(
graph
.find_function_at_location(file, 10)
.as_ref()
.map(|f| &f.name),
Some(&"function_one".to_string())
);
assert_eq!(
graph
.find_function_at_location(file, 30)
.as_ref()
.map(|f| &f.name),
Some(&"function_two".to_string())
);
assert_eq!(
graph
.find_function_at_location(file, 35)
.as_ref()
.map(|f| &f.name),
Some(&"function_two".to_string())
);
assert_eq!(graph.find_function_at_location(file, 5), None);
}
#[test]
fn test_delegation_detection() {
let mut graph = CallGraph::new();
let (orchestrator, workers) = create_delegation_setup();
setup_delegation_graph(&mut graph, &orchestrator, &workers);
assert!(graph.detect_delegation_pattern(&orchestrator));
}
fn create_delegation_setup() -> (FunctionId, Vec<FunctionId>) {
let orchestrator = FunctionId::new(PathBuf::from("orch.rs"), "orchestrate".to_string(), 1);
let workers = vec![
FunctionId::new(PathBuf::from("work.rs"), "complex_work1".to_string(), 10),
FunctionId::new(PathBuf::from("work.rs"), "complex_work2".to_string(), 20),
];
(orchestrator, workers)
}
fn setup_delegation_graph(
graph: &mut CallGraph,
orchestrator: &FunctionId,
workers: &[FunctionId],
) {
graph.add_function(orchestrator.clone(), false, false, 2, 15);
graph.add_function(workers[0].clone(), false, false, 10, 50);
graph.add_function(workers[1].clone(), false, false, 8, 40);
for worker in workers {
graph.add_call(FunctionCall {
caller: orchestrator.clone(),
callee: worker.clone(),
call_type: CallType::Delegate,
});
}
}
#[test]
fn test_get_transitive_callers_single_level() {
let mut graph = CallGraph::new();
let functions = create_caller_test_functions();
setup_single_level_callers(&mut graph, &functions);
verify_single_level_callers(&graph, &functions);
}
fn create_caller_test_functions() -> Vec<FunctionId> {
vec![
FunctionId::new(PathBuf::from("test.rs"), "func_a".to_string(), 1),
FunctionId::new(PathBuf::from("test.rs"), "func_b".to_string(), 10),
FunctionId::new(PathBuf::from("test.rs"), "func_c".to_string(), 20),
]
}
fn setup_single_level_callers(graph: &mut CallGraph, funcs: &[FunctionId]) {
for func in funcs {
graph.add_function(func.clone(), false, false, 1, 10);
}
add_call_edge(graph, &funcs[0], &funcs[1]);
add_call_edge(graph, &funcs[2], &funcs[1]);
}
fn verify_single_level_callers(graph: &CallGraph, funcs: &[FunctionId]) {
let callers = graph.get_transitive_callers(&funcs[1], 1);
assert_eq!(callers.len(), 2);
assert!(callers.contains(&funcs[0]));
assert!(callers.contains(&funcs[2]));
}
#[test]
fn test_get_transitive_callers_multi_level() {
let mut graph = CallGraph::new();
let chain = create_caller_chain();
setup_caller_chain(&mut graph, &chain);
verify_multi_level_callers(&graph, &chain);
}
fn create_caller_chain() -> Vec<FunctionId> {
vec![
FunctionId::new(PathBuf::from("test.rs"), "func_a".to_string(), 1),
FunctionId::new(PathBuf::from("test.rs"), "func_b".to_string(), 10),
FunctionId::new(PathBuf::from("test.rs"), "func_c".to_string(), 20),
FunctionId::new(PathBuf::from("test.rs"), "func_d".to_string(), 30),
]
}
fn setup_caller_chain(graph: &mut CallGraph, chain: &[FunctionId]) {
for func in chain {
graph.add_function(func.clone(), false, false, 1, 10);
}
for i in 0..chain.len() - 1 {
add_call_edge(graph, &chain[i], &chain[i + 1]);
}
}
fn verify_multi_level_callers(graph: &CallGraph, chain: &[FunctionId]) {
let callers = graph.get_transitive_callers(&chain[3], 3);
assert_eq!(callers.len(), 3);
for node in chain.iter().take(3) {
assert!(callers.contains(node));
}
let callers_depth_1 = graph.get_transitive_callers(&chain[3], 1);
assert_eq!(callers_depth_1.len(), 1);
assert!(callers_depth_1.contains(&chain[2]));
}
#[test]
fn test_get_transitive_callers_with_cycles() {
let mut graph = CallGraph::new();
let cycle = create_cycle_functions();
setup_cycle_graph(&mut graph, &cycle);
verify_cycle_handling(&graph, &cycle);
}
fn create_cycle_functions() -> Vec<FunctionId> {
vec![
FunctionId::new(PathBuf::from("test.rs"), "func_a".to_string(), 1),
FunctionId::new(PathBuf::from("test.rs"), "func_b".to_string(), 10),
FunctionId::new(PathBuf::from("test.rs"), "func_c".to_string(), 20),
]
}
fn setup_cycle_graph(graph: &mut CallGraph, funcs: &[FunctionId]) {
for func in funcs {
graph.add_function(func.clone(), false, false, 1, 10);
}
add_call_edge(graph, &funcs[0], &funcs[1]);
add_call_edge(graph, &funcs[1], &funcs[2]);
add_call_edge(graph, &funcs[2], &funcs[0]);
}
fn verify_cycle_handling(graph: &CallGraph, funcs: &[FunctionId]) {
let callers = graph.get_transitive_callers(&funcs[0], 10);
assert_eq!(callers.len(), 2);
assert!(callers.contains(&funcs[1]));
assert!(callers.contains(&funcs[2]));
}
#[test]
fn test_get_transitive_callers_no_callers() {
let mut graph = CallGraph::new();
let (a, b) = create_simple_pair();
setup_simple_graph(&mut graph, &a, &b);
verify_no_callers(&graph, &a);
}
fn create_simple_pair() -> (FunctionId, FunctionId) {
let a = FunctionId::new(PathBuf::from("test.rs"), "func_a".to_string(), 1);
let b = FunctionId::new(PathBuf::from("test.rs"), "func_b".to_string(), 10);
(a, b)
}
fn setup_simple_graph(graph: &mut CallGraph, a: &FunctionId, b: &FunctionId) {
graph.add_function(a.clone(), false, false, 1, 10);
graph.add_function(b.clone(), false, false, 1, 10);
add_call_edge(graph, a, b);
}
fn verify_no_callers(graph: &CallGraph, func: &FunctionId) {
let callers = graph.get_transitive_callers(func, 5);
assert_eq!(callers.len(), 0);
}
#[test]
fn test_get_transitive_callers_complex_graph() {
let mut graph = CallGraph::new();
let nodes = create_complex_graph_nodes();
setup_complex_graph(&mut graph, &nodes);
verify_complex_graph_callers(&graph, &nodes);
}
fn create_complex_graph_nodes() -> Vec<FunctionId> {
(0..6)
.map(|i| {
FunctionId::new(
PathBuf::from("test.rs"),
format!("func_{}", (b'a' + i) as char),
i as usize * 10 + 1,
)
})
.collect()
}
fn setup_complex_graph(graph: &mut CallGraph, nodes: &[FunctionId]) {
for node in nodes {
graph.add_function(node.clone(), false, false, 1, 10);
}
add_call_edge(graph, &nodes[0], &nodes[1]); add_call_edge(graph, &nodes[0], &nodes[2]); add_call_edge(graph, &nodes[1], &nodes[3]); add_call_edge(graph, &nodes[1], &nodes[4]); add_call_edge(graph, &nodes[2], &nodes[3]); add_call_edge(graph, &nodes[2], &nodes[4]); add_call_edge(graph, &nodes[3], &nodes[5]); add_call_edge(graph, &nodes[4], &nodes[5]); }
fn verify_complex_graph_callers(graph: &CallGraph, nodes: &[FunctionId]) {
let callers_f = graph.get_transitive_callers(&nodes[5], 10);
assert_eq!(callers_f.len(), 5); for node in nodes.iter().take(5) {
assert!(callers_f.contains(node));
}
let callers_f_depth_2 = graph.get_transitive_callers(&nodes[5], 2);
assert_eq!(callers_f_depth_2.len(), 4); }
#[test]
fn test_call_graph_serialization_roundtrip() {
let mut graph = CallGraph::new();
let (func1, func2) = create_serialization_test_functions();
setup_serialization_graph(&mut graph, &func1, &func2);
verify_serialization_roundtrip(&graph, &func1, &func2);
}
fn create_serialization_test_functions() -> (FunctionId, FunctionId) {
let func1 = FunctionId::new(PathBuf::from("src/main.rs"), "main".to_string(), 10);
let func2 = FunctionId::new(PathBuf::from("src/lib.rs"), "helper".to_string(), 25);
(func1, func2)
}
fn setup_serialization_graph(graph: &mut CallGraph, func1: &FunctionId, func2: &FunctionId) {
graph.add_function(func1.clone(), true, false, 5, 50);
graph.add_function(func2.clone(), false, false, 3, 30);
add_call_edge(graph, func1, func2);
}
fn verify_serialization_roundtrip(graph: &CallGraph, func1: &FunctionId, func2: &FunctionId) {
let json = serde_json::to_string(graph).unwrap();
let deserialized: CallGraph = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.get_callees(func1).len(), 1);
assert_eq!(deserialized.get_callers(func2).len(), 1);
assert!(deserialized.is_entry_point(func1));
assert!(!deserialized.is_entry_point(func2));
}
#[test]
fn test_is_cross_file_call_match() {
verify_exact_match();
verify_associated_function_match();
verify_method_with_type_context();
verify_suffix_matching();
verify_base_name_extraction();
verify_non_matches();
}
fn verify_exact_match() {
assert!(CallGraph::is_cross_file_call_match(
"my_function",
"my_function",
None
));
}
fn verify_associated_function_match() {
assert!(CallGraph::is_cross_file_call_match(
"ContextualRisk::new",
"ContextualRisk::new",
None
));
}
fn verify_method_with_type_context() {
assert!(CallGraph::is_cross_file_call_match(
"MyStruct::method",
"method",
Some("MyStruct")
));
}
fn verify_suffix_matching() {
assert!(CallGraph::is_cross_file_call_match(
"module::MyStruct::method",
"MyStruct::method",
None
));
}
fn verify_base_name_extraction() {
assert!(CallGraph::is_cross_file_call_match(
"MyStruct::new",
"new",
None
));
}
fn verify_non_matches() {
assert!(!CallGraph::is_cross_file_call_match(
"different_function",
"my_function",
None
));
assert!(!CallGraph::is_cross_file_call_match(
"MyStruct::method",
"other_method",
None
));
}
#[test]
fn test_select_best_cross_file_match() {
verify_single_candidate_selection();
verify_exact_match_preference();
verify_qualification_preference();
}
fn verify_single_candidate_selection() {
let caller_file = PathBuf::from("src/caller.rs");
let other_file = PathBuf::from("src/other.rs");
let single_candidate = vec![FunctionId::new(
other_file.clone(),
"test_func".to_string(),
10,
)];
let result = CallGraph::select_best_cross_file_match(
single_candidate.clone(),
&caller_file,
"test_func",
);
assert!(result.is_some());
assert_eq!(result.unwrap().name, "test_func");
}
fn verify_exact_match_preference() {
let caller_file = PathBuf::from("src/caller.rs");
let other_file = PathBuf::from("src/other.rs");
let third_file = PathBuf::from("src/third.rs");
let candidates = vec![
FunctionId::new(other_file.clone(), "test_func".to_string(), 10),
FunctionId::new(third_file.clone(), "MyStruct::test_func".to_string(), 20),
];
let result = CallGraph::select_best_cross_file_match(candidates, &caller_file, "test_func");
assert!(result.is_some());
assert_eq!(result.unwrap().name, "test_func");
}
fn verify_qualification_preference() {
let caller_file = PathBuf::from("src/caller.rs");
let other_file = PathBuf::from("src/other.rs");
let third_file = PathBuf::from("src/third.rs");
let candidates = vec![
FunctionId::new(
other_file.clone(),
"deep::module::MyStruct::method".to_string(),
10,
),
FunctionId::new(third_file.clone(), "MyStruct::method".to_string(), 20),
];
let result = CallGraph::select_best_cross_file_match(candidates, &caller_file, "method");
assert!(result.is_some());
assert_eq!(result.unwrap().name, "MyStruct::method");
}
#[test]
fn test_is_test_helper_detection() {
let mut graph = CallGraph::new();
let functions = create_test_helper_setup();
setup_test_helper_graph(&mut graph, &functions);
verify_test_helper_detection(&graph, &functions);
}
fn create_test_helper_setup() -> Vec<FunctionId> {
vec![
FunctionId::new(
PathBuf::from("tests/test.rs"),
"test_something".to_string(),
10,
),
FunctionId::new(
PathBuf::from("tests/test.rs"),
"test_another".to_string(),
30,
),
FunctionId::new(
PathBuf::from("src/lib.rs"),
"validate_initial_state".to_string(),
100,
),
FunctionId::new(PathBuf::from("src/lib.rs"), "process_data".to_string(), 200),
FunctionId::new(PathBuf::from("src/main.rs"), "main".to_string(), 1),
]
}
fn setup_test_helper_graph(graph: &mut CallGraph, funcs: &[FunctionId]) {
graph.add_function(funcs[0].clone(), false, true, 3, 20); graph.add_function(funcs[1].clone(), false, true, 4, 25); graph.add_function(funcs[2].clone(), false, false, 5, 30); graph.add_function(funcs[3].clone(), false, false, 6, 40); graph.add_function(funcs[4].clone(), true, false, 2, 15);
add_call_edge(graph, &funcs[0], &funcs[2]);
add_call_edge(graph, &funcs[1], &funcs[2]);
add_call_edge(graph, &funcs[4], &funcs[3]);
}
fn verify_test_helper_detection(graph: &CallGraph, funcs: &[FunctionId]) {
assert!(
graph.is_test_helper(&funcs[2]),
"validate_initial_state should be identified as a test helper"
);
assert!(
!graph.is_test_helper(&funcs[3]),
"process_data should not be identified as a test helper"
);
assert!(
!graph.is_test_helper(&funcs[0]),
"Test functions should not be identified as test helpers"
);
}