use std::collections::{HashMap, HashSet};
use crate::types::{FunctionRef, ProjectCallGraph};
pub fn build_reverse_graph(
call_graph: &ProjectCallGraph,
) -> HashMap<FunctionRef, Vec<FunctionRef>> {
let mut reverse: HashMap<FunctionRef, Vec<FunctionRef>> = HashMap::new();
for edge in call_graph.edges() {
let callee = FunctionRef::new(edge.dst_file.clone(), edge.dst_func.clone());
let caller = FunctionRef::new(edge.src_file.clone(), edge.src_func.clone());
reverse.entry(callee).or_default().push(caller);
}
reverse
}
pub fn build_forward_graph(
call_graph: &ProjectCallGraph,
) -> HashMap<FunctionRef, Vec<FunctionRef>> {
let mut forward: HashMap<FunctionRef, Vec<FunctionRef>> = HashMap::new();
for edge in call_graph.edges() {
let caller = FunctionRef::new(edge.src_file.clone(), edge.src_func.clone());
let callee = FunctionRef::new(edge.dst_file.clone(), edge.dst_func.clone());
forward.entry(caller).or_default().push(callee);
}
forward
}
pub fn collect_nodes(call_graph: &ProjectCallGraph) -> HashSet<FunctionRef> {
let mut nodes = HashSet::new();
for edge in call_graph.edges() {
nodes.insert(FunctionRef::new(
edge.src_file.clone(),
edge.src_func.clone(),
));
nodes.insert(FunctionRef::new(
edge.dst_file.clone(),
edge.dst_func.clone(),
));
}
nodes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::CallEdge;
use std::path::PathBuf;
fn create_test_graph() -> ProjectCallGraph {
let mut graph = ProjectCallGraph::new();
graph.add_edge(CallEdge {
src_file: PathBuf::from("a.py"),
src_func: "func_a".to_string(),
dst_file: PathBuf::from("b.py"),
dst_func: "func_b".to_string(),
});
graph.add_edge(CallEdge {
src_file: PathBuf::from("b.py"),
src_func: "func_b".to_string(),
dst_file: PathBuf::from("c.py"),
dst_func: "func_c".to_string(),
});
graph.add_edge(CallEdge {
src_file: PathBuf::from("d.py"),
src_func: "func_d".to_string(),
dst_file: PathBuf::from("c.py"),
dst_func: "func_c".to_string(),
});
graph
}
fn create_diamond_graph() -> ProjectCallGraph {
let mut graph = ProjectCallGraph::new();
graph.add_edge(CallEdge {
src_file: PathBuf::from("a.py"),
src_func: "func_a".to_string(),
dst_file: PathBuf::from("b.py"),
dst_func: "func_b".to_string(),
});
graph.add_edge(CallEdge {
src_file: PathBuf::from("a.py"),
src_func: "func_a".to_string(),
dst_file: PathBuf::from("c.py"),
dst_func: "func_c".to_string(),
});
graph.add_edge(CallEdge {
src_file: PathBuf::from("b.py"),
src_func: "func_b".to_string(),
dst_file: PathBuf::from("d.py"),
dst_func: "func_d".to_string(),
});
graph.add_edge(CallEdge {
src_file: PathBuf::from("c.py"),
src_func: "func_c".to_string(),
dst_file: PathBuf::from("d.py"),
dst_func: "func_d".to_string(),
});
graph
}
#[test]
fn test_forward_reverse_consistency() {
let graph = create_test_graph();
let forward = build_forward_graph(&graph);
let reverse = build_reverse_graph(&graph);
for (caller, callees) in &forward {
for callee in callees {
let callers_of_callee = reverse
.get(callee)
.expect("callee should be in reverse graph");
assert!(
callers_of_callee.contains(caller),
"Forward edge {:?} -> {:?} should have reverse entry",
caller,
callee
);
}
}
for (callee, callers) in &reverse {
for caller in callers {
let callees_of_caller = forward
.get(caller)
.expect("caller should be in forward graph");
assert!(
callees_of_caller.contains(callee),
"Reverse edge {:?} -> {:?} should have forward entry",
callee,
caller
);
}
}
}
#[test]
fn test_collect_nodes_unique() {
let graph = create_test_graph();
let nodes = collect_nodes(&graph);
assert_eq!(nodes.len(), 4);
assert!(nodes.contains(&FunctionRef::new(PathBuf::from("a.py"), "func_a")));
assert!(nodes.contains(&FunctionRef::new(PathBuf::from("b.py"), "func_b")));
assert!(nodes.contains(&FunctionRef::new(PathBuf::from("c.py"), "func_c")));
assert!(nodes.contains(&FunctionRef::new(PathBuf::from("d.py"), "func_d")));
}
#[test]
fn test_empty_graph_handling() {
let graph = ProjectCallGraph::new();
let forward = build_forward_graph(&graph);
let reverse = build_reverse_graph(&graph);
let nodes = collect_nodes(&graph);
assert!(
forward.is_empty(),
"Forward graph of empty graph should be empty"
);
assert!(
reverse.is_empty(),
"Reverse graph of empty graph should be empty"
);
assert!(nodes.is_empty(), "Nodes of empty graph should be empty");
}
#[test]
fn test_forward_graph_structure() {
let graph = create_test_graph();
let forward = build_forward_graph(&graph);
let a = FunctionRef::new(PathBuf::from("a.py"), "func_a");
assert_eq!(forward.get(&a).map(|v| v.len()), Some(1));
let b = FunctionRef::new(PathBuf::from("b.py"), "func_b");
assert_eq!(forward.get(&b).map(|v| v.len()), Some(1));
let d = FunctionRef::new(PathBuf::from("d.py"), "func_d");
assert_eq!(forward.get(&d).map(|v| v.len()), Some(1));
let c = FunctionRef::new(PathBuf::from("c.py"), "func_c");
assert!(!forward.contains_key(&c));
}
#[test]
fn test_reverse_graph_structure() {
let graph = create_test_graph();
let reverse = build_reverse_graph(&graph);
let c = FunctionRef::new(PathBuf::from("c.py"), "func_c");
assert_eq!(reverse.get(&c).map(|v| v.len()), Some(2));
let b = FunctionRef::new(PathBuf::from("b.py"), "func_b");
assert_eq!(reverse.get(&b).map(|v| v.len()), Some(1));
let a = FunctionRef::new(PathBuf::from("a.py"), "func_a");
assert!(!reverse.contains_key(&a));
let d = FunctionRef::new(PathBuf::from("d.py"), "func_d");
assert!(!reverse.contains_key(&d));
}
#[test]
fn test_diamond_graph_nodes() {
let graph = create_diamond_graph();
let nodes = collect_nodes(&graph);
assert_eq!(nodes.len(), 4);
}
#[test]
fn test_diamond_forward_out_degrees() {
let graph = create_diamond_graph();
let forward = build_forward_graph(&graph);
let a = FunctionRef::new(PathBuf::from("a.py"), "func_a");
assert_eq!(forward.get(&a).map(|v| v.len()), Some(2));
let b = FunctionRef::new(PathBuf::from("b.py"), "func_b");
assert_eq!(forward.get(&b).map(|v| v.len()), Some(1));
let c = FunctionRef::new(PathBuf::from("c.py"), "func_c");
assert_eq!(forward.get(&c).map(|v| v.len()), Some(1));
let d = FunctionRef::new(PathBuf::from("d.py"), "func_d");
assert!(!forward.contains_key(&d));
}
#[test]
fn test_diamond_reverse_in_degrees() {
let graph = create_diamond_graph();
let reverse = build_reverse_graph(&graph);
let d = FunctionRef::new(PathBuf::from("d.py"), "func_d");
assert_eq!(reverse.get(&d).map(|v| v.len()), Some(2));
let b = FunctionRef::new(PathBuf::from("b.py"), "func_b");
assert_eq!(reverse.get(&b).map(|v| v.len()), Some(1));
let c = FunctionRef::new(PathBuf::from("c.py"), "func_c");
assert_eq!(reverse.get(&c).map(|v| v.len()), Some(1));
let a = FunctionRef::new(PathBuf::from("a.py"), "func_a");
assert!(!reverse.contains_key(&a));
}
#[test]
fn test_self_loop_handling() {
let mut graph = ProjectCallGraph::new();
graph.add_edge(CallEdge {
src_file: PathBuf::from("a.py"),
src_func: "func_a".to_string(),
dst_file: PathBuf::from("a.py"),
dst_func: "func_a".to_string(),
});
let forward = build_forward_graph(&graph);
let reverse = build_reverse_graph(&graph);
let nodes = collect_nodes(&graph);
assert_eq!(nodes.len(), 1);
let a = FunctionRef::new(PathBuf::from("a.py"), "func_a");
assert_eq!(forward.get(&a).map(|v| v.len()), Some(1));
assert!(forward.get(&a).unwrap().contains(&a));
assert_eq!(reverse.get(&a).map(|v| v.len()), Some(1));
assert!(reverse.get(&a).unwrap().contains(&a));
}
#[test]
fn test_multiple_edges_same_pair() {
let mut graph = ProjectCallGraph::new();
graph.add_edge(CallEdge {
src_file: PathBuf::from("a.py"),
src_func: "func_a".to_string(),
dst_file: PathBuf::from("b.py"),
dst_func: "func_b".to_string(),
});
graph.add_edge(CallEdge {
src_file: PathBuf::from("a.py"),
src_func: "func_a".to_string(),
dst_file: PathBuf::from("b.py"),
dst_func: "func_b".to_string(),
});
assert_eq!(graph.edge_count(), 1);
let forward = build_forward_graph(&graph);
let a = FunctionRef::new(PathBuf::from("a.py"), "func_a");
assert_eq!(forward.get(&a).map(|v| v.len()), Some(1));
}
}