refactor: modularize codebase and add 404 unit tests (#13)
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m19s
CI / Security Audit (push) Successful in 1m44s
CI / Tests (push) Successful in 5m15s
CI / Detect Changes (push) Successful in 5s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2s

This commit was merged in pull request #13.
This commit is contained in:
2026-03-13 08:03:45 +00:00
parent acc5b86aa4
commit 3bb690e5bb
89 changed files with 11884 additions and 6046 deletions
+375
View File
@@ -222,3 +222,378 @@ impl<'a> ImpactAnalyzer<'a> {
.find(|n| n.graph_index == Some(target_gi))
}
}
#[cfg(test)]
mod tests {
use super::*;
use compliance_core::models::graph::{CodeEdgeKind, CodeNode, CodeNodeKind};
use petgraph::graph::DiGraph;
use std::collections::HashMap;
fn make_node(
qualified_name: &str,
file_path: &str,
start: u32,
end: u32,
graph_index: u32,
is_entry: bool,
kind: CodeNodeKind,
) -> CodeNode {
CodeNode {
id: None,
repo_id: "test".to_string(),
graph_build_id: "build1".to_string(),
qualified_name: qualified_name.to_string(),
name: qualified_name
.split("::")
.last()
.unwrap_or(qualified_name)
.to_string(),
kind,
file_path: file_path.to_string(),
start_line: start,
end_line: end,
language: "rust".to_string(),
community_id: None,
is_entry_point: is_entry,
graph_index: Some(graph_index),
}
}
fn make_fn_node(
qualified_name: &str,
file_path: &str,
start: u32,
end: u32,
gi: u32,
) -> CodeNode {
make_node(
qualified_name,
file_path,
start,
end,
gi,
false,
CodeNodeKind::Function,
)
}
/// Build a simple linear graph: A -> B -> C
fn build_linear_graph() -> CodeGraph {
let mut graph = DiGraph::new();
let a = graph.add_node("a".to_string());
let b = graph.add_node("b".to_string());
let c = graph.add_node("c".to_string());
graph.add_edge(a, b, CodeEdgeKind::Calls);
graph.add_edge(b, c, CodeEdgeKind::Calls);
let mut node_map = HashMap::new();
node_map.insert("a".to_string(), a);
node_map.insert("b".to_string(), b);
node_map.insert("c".to_string(), c);
CodeGraph {
graph,
node_map,
nodes: vec![
make_fn_node("a", "src/main.rs", 1, 5, 0),
make_fn_node("b", "src/main.rs", 7, 12, 1),
make_fn_node("c", "src/main.rs", 14, 20, 2),
],
edges: Vec::new(),
}
}
#[test]
fn test_bfs_reachable_outgoing_linear() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let start = cg.node_map["a"];
let reachable = analyzer.bfs_reachable(start, Direction::Outgoing);
// From a, we can reach b and c
assert_eq!(reachable.len(), 2);
assert!(reachable.contains(&cg.node_map["b"]));
assert!(reachable.contains(&cg.node_map["c"]));
}
#[test]
fn test_bfs_reachable_incoming_linear() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let start = cg.node_map["c"];
let reachable = analyzer.bfs_reachable(start, Direction::Incoming);
// c is reached by a and b
assert_eq!(reachable.len(), 2);
assert!(reachable.contains(&cg.node_map["a"]));
assert!(reachable.contains(&cg.node_map["b"]));
}
#[test]
fn test_bfs_reachable_no_neighbors() {
let mut graph = DiGraph::new();
let a = graph.add_node("a".to_string());
let cg = CodeGraph {
graph,
node_map: [("a".to_string(), a)].into_iter().collect(),
nodes: vec![make_fn_node("a", "src/main.rs", 1, 5, 0)],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
let reachable = analyzer.bfs_reachable(a, Direction::Outgoing);
assert!(reachable.is_empty());
}
#[test]
fn test_bfs_reachable_cycle() {
let mut graph = DiGraph::new();
let a = graph.add_node("a".to_string());
let b = graph.add_node("b".to_string());
graph.add_edge(a, b, CodeEdgeKind::Calls);
graph.add_edge(b, a, CodeEdgeKind::Calls);
let cg = CodeGraph {
graph,
node_map: [("a".to_string(), a), ("b".to_string(), b)]
.into_iter()
.collect(),
nodes: vec![
make_fn_node("a", "f.rs", 1, 5, 0),
make_fn_node("b", "f.rs", 6, 10, 1),
],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
let reachable = analyzer.bfs_reachable(a, Direction::Outgoing);
// Should handle cycle without infinite loop
assert_eq!(reachable.len(), 1);
assert!(reachable.contains(&b));
}
#[test]
fn test_find_path_exists() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let path = analyzer.find_path(cg.node_map["a"], cg.node_map["c"], 10);
assert!(path.is_some());
let names = path.unwrap();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn test_find_path_direct() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let path = analyzer.find_path(cg.node_map["a"], cg.node_map["b"], 10);
assert!(path.is_some());
let names = path.unwrap();
assert_eq!(names, vec!["a", "b"]);
}
#[test]
fn test_find_path_same_node() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let path = analyzer.find_path(cg.node_map["a"], cg.node_map["a"], 10);
assert!(path.is_some());
let names = path.unwrap();
assert_eq!(names, vec!["a"]);
}
#[test]
fn test_find_path_no_connection() {
let mut graph = DiGraph::new();
let a = graph.add_node("a".to_string());
let b = graph.add_node("b".to_string());
// No edge between a and b
let cg = CodeGraph {
graph,
node_map: [("a".to_string(), a), ("b".to_string(), b)]
.into_iter()
.collect(),
nodes: vec![
make_fn_node("a", "f.rs", 1, 5, 0),
make_fn_node("b", "f.rs", 6, 10, 1),
],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
let path = analyzer.find_path(a, b, 10);
assert!(path.is_none());
}
#[test]
fn test_find_path_depth_limited() {
// Build a long chain: a -> b -> c -> d -> e
let mut graph = DiGraph::new();
let a = graph.add_node("a".to_string());
let b = graph.add_node("b".to_string());
let c = graph.add_node("c".to_string());
let d = graph.add_node("d".to_string());
let e = graph.add_node("e".to_string());
graph.add_edge(a, b, CodeEdgeKind::Calls);
graph.add_edge(b, c, CodeEdgeKind::Calls);
graph.add_edge(c, d, CodeEdgeKind::Calls);
graph.add_edge(d, e, CodeEdgeKind::Calls);
let mut node_map = HashMap::new();
node_map.insert("a".to_string(), a);
node_map.insert("b".to_string(), b);
node_map.insert("c".to_string(), c);
node_map.insert("d".to_string(), d);
node_map.insert("e".to_string(), e);
let cg = CodeGraph {
graph,
node_map,
nodes: vec![
make_fn_node("a", "f.rs", 1, 2, 0),
make_fn_node("b", "f.rs", 3, 4, 1),
make_fn_node("c", "f.rs", 5, 6, 2),
make_fn_node("d", "f.rs", 7, 8, 3),
make_fn_node("e", "f.rs", 9, 10, 4),
],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
// Depth 3 won't reach e from a (path length 5)
let path = analyzer.find_path(a, e, 3);
assert!(path.is_none());
// Depth 5 should reach
let path = analyzer.find_path(a, e, 5);
assert!(path.is_some());
}
#[test]
fn test_find_node_at_location_exact_line() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
// Node "b" is at lines 7-12
let result = analyzer.find_node_at_location("src/main.rs", Some(9));
assert!(result.is_some());
assert_eq!(result.unwrap(), cg.node_map["b"]);
}
#[test]
fn test_find_node_at_location_narrowest_match() {
// Outer function 1-20, inner nested 5-10
let mut graph = DiGraph::new();
let outer = graph.add_node("outer".to_string());
let inner = graph.add_node("inner".to_string());
let cg = CodeGraph {
graph,
node_map: [("outer".to_string(), outer), ("inner".to_string(), inner)]
.into_iter()
.collect(),
nodes: vec![
make_fn_node("outer", "src/main.rs", 1, 20, 0),
make_fn_node("inner", "src/main.rs", 5, 10, 1),
],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
// Line 7 is inside both, but inner is narrower
let result = analyzer.find_node_at_location("src/main.rs", Some(7));
assert!(result.is_some());
assert_eq!(result.unwrap(), inner);
}
#[test]
fn test_find_node_at_location_no_line_returns_file_node() {
let mut graph = DiGraph::new();
let file_node = graph.add_node("src/main.rs".to_string());
let fn_node = graph.add_node("src/main.rs::foo".to_string());
let cg = CodeGraph {
graph,
node_map: [
("src/main.rs".to_string(), file_node),
("src/main.rs::foo".to_string(), fn_node),
]
.into_iter()
.collect(),
nodes: vec![
make_node(
"src/main.rs",
"src/main.rs",
1,
100,
0,
false,
CodeNodeKind::File,
),
make_fn_node("src/main.rs::foo", "src/main.rs", 5, 10, 1),
],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
let result = analyzer.find_node_at_location("src/main.rs", None);
assert!(result.is_some());
assert_eq!(result.unwrap(), file_node);
}
#[test]
fn test_find_node_at_location_wrong_file() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let result = analyzer.find_node_at_location("nonexistent.rs", Some(5));
assert!(result.is_none());
}
#[test]
fn test_find_node_at_location_line_out_of_range() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let result = analyzer.find_node_at_location("src/main.rs", Some(999));
assert!(result.is_none());
}
#[test]
fn test_analyze_basic() {
// A (entry) -> B -> C
let mut graph = DiGraph::new();
let a = graph.add_node("a".to_string());
let b = graph.add_node("b".to_string());
let c = graph.add_node("c".to_string());
graph.add_edge(a, b, CodeEdgeKind::Calls);
graph.add_edge(b, c, CodeEdgeKind::Calls);
let mut node_map = HashMap::new();
node_map.insert("a".to_string(), a);
node_map.insert("b".to_string(), b);
node_map.insert("c".to_string(), c);
let cg = CodeGraph {
graph,
node_map,
nodes: vec![
make_node("a", "src/main.rs", 1, 5, 0, true, CodeNodeKind::Function),
make_fn_node("b", "src/main.rs", 7, 12, 1),
make_fn_node("c", "src/main.rs", 14, 20, 2),
],
edges: Vec::new(),
};
let analyzer = ImpactAnalyzer::new(&cg);
let result = analyzer.analyze("repo1", "finding1", "build1", "src/main.rs", Some(9));
// B's blast radius: C is reachable forward
assert_eq!(result.blast_radius, 1);
// B has A as direct caller
assert_eq!(result.direct_callers, vec!["a"]);
// B calls C
assert_eq!(result.direct_callees, vec!["c"]);
// A is an entry point that reaches B
assert_eq!(result.affected_entry_points, vec!["a"]);
}
#[test]
fn test_analyze_no_matching_node() {
let cg = build_linear_graph();
let analyzer = ImpactAnalyzer::new(&cg);
let result = analyzer.analyze("repo1", "f1", "b1", "nonexistent.rs", Some(1));
assert_eq!(result.blast_radius, 0);
assert!(result.affected_entry_points.is_empty());
assert!(result.direct_callers.is_empty());
assert!(result.direct_callees.is_empty());
}
}