refactor: modularize codebase and add 404 unit tests (#13)
All checks were successful
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m19s
CI / Security Audit (push) Successful in 1m44s
CI / Detect Changes (push) Successful in 5s
CI / Tests (push) Successful in 5m15s
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
All checks were successful
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m19s
CI / Security Audit (push) Successful in 1m44s
CI / Detect Changes (push) Successful in 5s
CI / Tests (push) Successful in 5m15s
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:
@@ -94,3 +94,64 @@ fn build_context_header(file_path: &str, qualified_name: &str, kind: &str) -> St
|
||||
format!("// {file_path} | {kind}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_context_header_with_parent() {
|
||||
let result =
|
||||
build_context_header("src/main.rs", "src/main.rs::MyStruct::my_method", "method");
|
||||
assert_eq!(result, "// src/main.rs | method in src/main.rs::MyStruct");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_context_header_top_level() {
|
||||
let result = build_context_header("src/lib.rs", "main", "function");
|
||||
assert_eq!(result, "// src/lib.rs | function");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_context_header_single_parent() {
|
||||
let result = build_context_header("src/lib.rs", "src/lib.rs::do_stuff", "function");
|
||||
assert_eq!(result, "// src/lib.rs | function in src/lib.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_context_header_deep_nesting() {
|
||||
let result = build_context_header(
|
||||
"src/mod.rs",
|
||||
"src/mod.rs::Outer::Inner::deep_fn",
|
||||
"function",
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
"// src/mod.rs | function in src/mod.rs::Outer::Inner"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_context_header_empty_strings() {
|
||||
let result = build_context_header("", "", "function");
|
||||
assert_eq!(result, "// | function");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_chunk_struct_fields() {
|
||||
let chunk = CodeChunk {
|
||||
qualified_name: "main".to_string(),
|
||||
kind: "function".to_string(),
|
||||
file_path: "src/main.rs".to_string(),
|
||||
start_line: 1,
|
||||
end_line: 10,
|
||||
language: "rust".to_string(),
|
||||
content: "fn main() {}".to_string(),
|
||||
context_header: "// src/main.rs | function".to_string(),
|
||||
token_estimate: 3,
|
||||
};
|
||||
assert_eq!(chunk.start_line, 1);
|
||||
assert_eq!(chunk.end_line, 10);
|
||||
assert_eq!(chunk.language, "rust");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,3 +253,215 @@ fn detect_communities_with_assignment(code_graph: &mut CodeGraph) -> u32 {
|
||||
|
||||
next_id
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use compliance_core::models::graph::{CodeEdgeKind, CodeNode, CodeNodeKind};
|
||||
use petgraph::graph::DiGraph;
|
||||
|
||||
fn make_node(qualified_name: &str, graph_index: u32) -> CodeNode {
|
||||
CodeNode {
|
||||
id: None,
|
||||
repo_id: "test".to_string(),
|
||||
graph_build_id: "build1".to_string(),
|
||||
qualified_name: qualified_name.to_string(),
|
||||
name: qualified_name.to_string(),
|
||||
kind: CodeNodeKind::Function,
|
||||
file_path: "test.rs".to_string(),
|
||||
start_line: 1,
|
||||
end_line: 10,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: Some(graph_index),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_empty_code_graph() -> CodeGraph {
|
||||
CodeGraph {
|
||||
graph: DiGraph::new(),
|
||||
node_map: HashMap::new(),
|
||||
nodes: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_communities_empty_graph() {
|
||||
let cg = make_empty_code_graph();
|
||||
assert_eq!(detect_communities(&cg), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_communities_single_node_no_edges() {
|
||||
let mut graph = DiGraph::new();
|
||||
let idx = graph.add_node("a".to_string());
|
||||
let mut node_map = HashMap::new();
|
||||
node_map.insert("a".to_string(), idx);
|
||||
|
||||
let cg = CodeGraph {
|
||||
graph,
|
||||
node_map,
|
||||
nodes: vec![make_node("a", 0)],
|
||||
edges: Vec::new(),
|
||||
};
|
||||
// Single node with no edges => 1 community (itself)
|
||||
assert_eq!(detect_communities(&cg), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_communities_isolated_nodes() {
|
||||
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 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", 0), make_node("b", 1), make_node("c", 2)],
|
||||
edges: Vec::new(),
|
||||
};
|
||||
// 3 isolated nodes => 3 communities
|
||||
assert_eq!(detect_communities(&cg), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_communities_fully_connected() {
|
||||
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);
|
||||
graph.add_edge(c, a, 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", 0), make_node("b", 1), make_node("c", 2)],
|
||||
edges: Vec::new(),
|
||||
};
|
||||
let num = detect_communities(&cg);
|
||||
// Fully connected triangle should converge to 1 community
|
||||
assert!(num >= 1);
|
||||
assert!(num <= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_communities_two_clusters() {
|
||||
// Two separate triangles connected by a single weak edge
|
||||
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());
|
||||
let f = graph.add_node("f".to_string());
|
||||
|
||||
// Cluster 1: a-b-c fully connected
|
||||
graph.add_edge(a, b, CodeEdgeKind::Calls);
|
||||
graph.add_edge(b, a, CodeEdgeKind::Calls);
|
||||
graph.add_edge(b, c, CodeEdgeKind::Calls);
|
||||
graph.add_edge(c, b, CodeEdgeKind::Calls);
|
||||
graph.add_edge(a, c, CodeEdgeKind::Calls);
|
||||
graph.add_edge(c, a, CodeEdgeKind::Calls);
|
||||
|
||||
// Cluster 2: d-e-f fully connected
|
||||
graph.add_edge(d, e, CodeEdgeKind::Calls);
|
||||
graph.add_edge(e, d, CodeEdgeKind::Calls);
|
||||
graph.add_edge(e, f, CodeEdgeKind::Calls);
|
||||
graph.add_edge(f, e, CodeEdgeKind::Calls);
|
||||
graph.add_edge(d, f, CodeEdgeKind::Calls);
|
||||
graph.add_edge(f, d, 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);
|
||||
node_map.insert("f".to_string(), f);
|
||||
|
||||
let cg = CodeGraph {
|
||||
graph,
|
||||
node_map,
|
||||
nodes: vec![
|
||||
make_node("a", 0),
|
||||
make_node("b", 1),
|
||||
make_node("c", 2),
|
||||
make_node("d", 3),
|
||||
make_node("e", 4),
|
||||
make_node("f", 5),
|
||||
],
|
||||
edges: Vec::new(),
|
||||
};
|
||||
let num = detect_communities(&cg);
|
||||
// Two disconnected clusters should yield 2 communities
|
||||
assert_eq!(num, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_communities_assigns_ids() {
|
||||
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);
|
||||
|
||||
let mut node_map = HashMap::new();
|
||||
node_map.insert("a".to_string(), a);
|
||||
node_map.insert("b".to_string(), b);
|
||||
|
||||
let mut cg = CodeGraph {
|
||||
graph,
|
||||
node_map,
|
||||
nodes: vec![make_node("a", 0), make_node("b", 1)],
|
||||
edges: Vec::new(),
|
||||
};
|
||||
let count = apply_communities(&mut cg);
|
||||
assert!(count >= 1);
|
||||
// All nodes should have a community_id assigned
|
||||
for node in &cg.nodes {
|
||||
assert!(node.community_id.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_communities_empty() {
|
||||
let mut cg = make_empty_code_graph();
|
||||
assert_eq!(apply_communities(&mut cg), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_communities_isolated_nodes_get_own_community() {
|
||||
let mut graph = DiGraph::new();
|
||||
let a = graph.add_node("a".to_string());
|
||||
let b = graph.add_node("b".to_string());
|
||||
let mut node_map = HashMap::new();
|
||||
node_map.insert("a".to_string(), a);
|
||||
node_map.insert("b".to_string(), b);
|
||||
|
||||
let mut cg = CodeGraph {
|
||||
graph,
|
||||
node_map,
|
||||
nodes: vec![make_node("a", 0), make_node("b", 1)],
|
||||
edges: Vec::new(),
|
||||
};
|
||||
let count = apply_communities(&mut cg);
|
||||
assert_eq!(count, 2);
|
||||
// Each isolated node should be in a different community
|
||||
let c0 = cg.nodes[0].community_id.unwrap();
|
||||
let c1 = cg.nodes[1].community_id.unwrap();
|
||||
assert_ne!(c0, c1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,3 +172,185 @@ impl GraphEngine {
|
||||
ImpactAnalyzer::new(code_graph)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use compliance_core::models::graph::{CodeEdgeKind, CodeNode, CodeNodeKind};
|
||||
|
||||
fn make_node(qualified_name: &str) -> 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: CodeNodeKind::Function,
|
||||
file_path: "src/main.rs".to_string(),
|
||||
start_line: 1,
|
||||
end_line: 10,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_test_node_map(names: &[&str]) -> HashMap<String, NodeIndex> {
|
||||
let mut graph: DiGraph<String, String> = DiGraph::new();
|
||||
let mut map = HashMap::new();
|
||||
for name in names {
|
||||
let idx = graph.add_node(name.to_string());
|
||||
map.insert(name.to_string(), idx);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_direct_match() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = build_test_node_map(&["src/main.rs::foo", "src/main.rs::bar"]);
|
||||
let result = engine.resolve_edge_target("src/main.rs::foo", &node_map);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), node_map["src/main.rs::foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_short_name_match() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = build_test_node_map(&["src/main.rs::foo", "src/main.rs::bar"]);
|
||||
let result = engine.resolve_edge_target("foo", &node_map);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), node_map["src/main.rs::foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_method_match() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = build_test_node_map(&["src/main.rs::MyStruct::do_thing"]);
|
||||
let result = engine.resolve_edge_target("do_thing", &node_map);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_self_method() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = build_test_node_map(&["src/main.rs::MyStruct::process"]);
|
||||
let result = engine.resolve_edge_target("self.process", &node_map);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_no_match() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = build_test_node_map(&["src/main.rs::foo"]);
|
||||
let result = engine.resolve_edge_target("nonexistent", &node_map);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_empty_map() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = HashMap::new();
|
||||
let result = engine.resolve_edge_target("anything", &node_map);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_edge_target_dot_notation() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let node_map = build_test_node_map(&["src/app.js.handler"]);
|
||||
let result = engine.resolve_edge_target("handler", &node_map);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_petgraph_empty() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let output = ParseOutput::default();
|
||||
let code_graph = engine.build_petgraph(output).unwrap();
|
||||
assert_eq!(code_graph.nodes.len(), 0);
|
||||
assert_eq!(code_graph.edges.len(), 0);
|
||||
assert_eq!(code_graph.graph.node_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_petgraph_nodes_get_graph_index() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let mut output = ParseOutput::default();
|
||||
output.nodes.push(make_node("src/main.rs::foo"));
|
||||
output.nodes.push(make_node("src/main.rs::bar"));
|
||||
|
||||
let code_graph = engine.build_petgraph(output).unwrap();
|
||||
assert_eq!(code_graph.nodes.len(), 2);
|
||||
assert_eq!(code_graph.graph.node_count(), 2);
|
||||
// All nodes should have a graph_index assigned
|
||||
for node in &code_graph.nodes {
|
||||
assert!(node.graph_index.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_petgraph_resolves_edges() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let mut output = ParseOutput::default();
|
||||
output.nodes.push(make_node("src/main.rs::foo"));
|
||||
output.nodes.push(make_node("src/main.rs::bar"));
|
||||
output.edges.push(CodeEdge {
|
||||
id: None,
|
||||
repo_id: "test".to_string(),
|
||||
graph_build_id: "build1".to_string(),
|
||||
source: "src/main.rs::foo".to_string(),
|
||||
target: "bar".to_string(), // short name, should resolve
|
||||
kind: CodeEdgeKind::Calls,
|
||||
file_path: "src/main.rs".to_string(),
|
||||
line_number: Some(5),
|
||||
});
|
||||
|
||||
let code_graph = engine.build_petgraph(output).unwrap();
|
||||
assert_eq!(code_graph.edges.len(), 1);
|
||||
assert_eq!(code_graph.graph.edge_count(), 1);
|
||||
// The resolved edge target should be the full qualified name
|
||||
assert_eq!(code_graph.edges[0].target, "src/main.rs::bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_petgraph_skips_unresolved_edges() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let mut output = ParseOutput::default();
|
||||
output.nodes.push(make_node("src/main.rs::foo"));
|
||||
output.edges.push(CodeEdge {
|
||||
id: None,
|
||||
repo_id: "test".to_string(),
|
||||
graph_build_id: "build1".to_string(),
|
||||
source: "src/main.rs::foo".to_string(),
|
||||
target: "external_crate::something".to_string(),
|
||||
kind: CodeEdgeKind::Calls,
|
||||
file_path: "src/main.rs".to_string(),
|
||||
line_number: Some(5),
|
||||
});
|
||||
|
||||
let code_graph = engine.build_petgraph(output).unwrap();
|
||||
assert_eq!(code_graph.edges.len(), 0);
|
||||
assert_eq!(code_graph.graph.edge_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_graph_node_map_consistency() {
|
||||
let engine = GraphEngine::new(1000);
|
||||
let mut output = ParseOutput::default();
|
||||
output.nodes.push(make_node("a::b"));
|
||||
output.nodes.push(make_node("a::c"));
|
||||
output.nodes.push(make_node("a::d"));
|
||||
|
||||
let code_graph = engine.build_petgraph(output).unwrap();
|
||||
assert_eq!(code_graph.node_map.len(), 3);
|
||||
assert!(code_graph.node_map.contains_key("a::b"));
|
||||
assert!(code_graph.node_map.contains_key("a::c"));
|
||||
assert!(code_graph.node_map.contains_key("a::d"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,3 +184,115 @@ impl Default for ParserRegistry {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_supports_rust_extension() {
|
||||
let registry = ParserRegistry::new();
|
||||
assert!(registry.supports_extension("rs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_python_extension() {
|
||||
let registry = ParserRegistry::new();
|
||||
assert!(registry.supports_extension("py"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_javascript_extension() {
|
||||
let registry = ParserRegistry::new();
|
||||
assert!(registry.supports_extension("js"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_typescript_extension() {
|
||||
let registry = ParserRegistry::new();
|
||||
assert!(registry.supports_extension("ts"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_does_not_support_unknown_extension() {
|
||||
let registry = ParserRegistry::new();
|
||||
assert!(!registry.supports_extension("go"));
|
||||
assert!(!registry.supports_extension("java"));
|
||||
assert!(!registry.supports_extension("cpp"));
|
||||
assert!(!registry.supports_extension(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supported_extensions_includes_all() {
|
||||
let registry = ParserRegistry::new();
|
||||
let exts = registry.supported_extensions();
|
||||
assert!(exts.contains(&"rs"));
|
||||
assert!(exts.contains(&"py"));
|
||||
assert!(exts.contains(&"js"));
|
||||
assert!(exts.contains(&"ts"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supported_extensions_count() {
|
||||
let registry = ParserRegistry::new();
|
||||
let exts = registry.supported_extensions();
|
||||
// At least 4 extensions (rs, py, js, ts); could be more if tsx, jsx etc.
|
||||
assert!(exts.len() >= 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_returns_none_for_unsupported() {
|
||||
let registry = ParserRegistry::new();
|
||||
let path = PathBuf::from("test.go");
|
||||
let result = registry.parse_file(&path, "package main", "repo1", "build1");
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_rust_source() {
|
||||
let registry = ParserRegistry::new();
|
||||
let path = PathBuf::from("src/main.rs");
|
||||
let source = "fn main() {\n println!(\"hello\");\n}\n";
|
||||
let result = registry.parse_file(&path, source, "repo1", "build1");
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
assert!(output.is_some());
|
||||
let output = output.unwrap();
|
||||
// Should have at least the file node and the main function node
|
||||
assert!(output.nodes.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_python_source() {
|
||||
let registry = ParserRegistry::new();
|
||||
let path = PathBuf::from("app.py");
|
||||
let source = "def hello():\n print('hi')\n";
|
||||
let result = registry.parse_file(&path, source, "repo1", "build1");
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
assert!(output.is_some());
|
||||
let output = output.unwrap();
|
||||
assert!(!output.nodes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_empty_source() {
|
||||
let registry = ParserRegistry::new();
|
||||
let path = PathBuf::from("empty.rs");
|
||||
let result = registry.parse_file(&path, "", "repo1", "build1");
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
assert!(output.is_some());
|
||||
// At minimum the file node
|
||||
let output = output.unwrap();
|
||||
assert!(!output.nodes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_trait() {
|
||||
let registry = ParserRegistry::default();
|
||||
assert!(registry.supports_extension("rs"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,214 @@ impl RustParser {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use compliance_core::traits::graph_builder::LanguageParser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn parse_rust(source: &str) -> ParseOutput {
|
||||
let parser = RustParser::new();
|
||||
parser
|
||||
.parse_file(&PathBuf::from("test.rs"), source, "repo1", "build1")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_use_path_simple() {
|
||||
let parser = RustParser::new();
|
||||
assert_eq!(
|
||||
parser.extract_use_path("use std::collections::HashMap;"),
|
||||
Some("std::collections::HashMap".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_use_path_nested() {
|
||||
let parser = RustParser::new();
|
||||
assert_eq!(
|
||||
parser.extract_use_path("use crate::models::graph::CodeNode;"),
|
||||
Some("crate::models::graph::CodeNode".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_use_path_no_prefix() {
|
||||
let parser = RustParser::new();
|
||||
assert_eq!(parser.extract_use_path("let x = 5;"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_use_path_empty() {
|
||||
let parser = RustParser::new();
|
||||
assert_eq!(parser.extract_use_path(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_function() {
|
||||
let output = parse_rust("fn hello() {\n let x = 1;\n}\n");
|
||||
let fn_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Function)
|
||||
.collect();
|
||||
assert_eq!(fn_nodes.len(), 1);
|
||||
assert_eq!(fn_nodes[0].name, "hello");
|
||||
assert!(fn_nodes[0].qualified_name.contains("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_struct() {
|
||||
let output = parse_rust("struct Foo {\n x: i32,\n}\n");
|
||||
let struct_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Struct)
|
||||
.collect();
|
||||
assert_eq!(struct_nodes.len(), 1);
|
||||
assert_eq!(struct_nodes[0].name, "Foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_enum() {
|
||||
let output = parse_rust("enum Color {\n Red,\n Blue,\n}\n");
|
||||
let enum_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Enum)
|
||||
.collect();
|
||||
assert_eq!(enum_nodes.len(), 1);
|
||||
assert_eq!(enum_nodes[0].name, "Color");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trait() {
|
||||
let output = parse_rust("trait Drawable {\n fn draw(&self);\n}\n");
|
||||
let trait_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Trait)
|
||||
.collect();
|
||||
assert_eq!(trait_nodes.len(), 1);
|
||||
assert_eq!(trait_nodes[0].name, "Drawable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_node_always_created() {
|
||||
let output = parse_rust("");
|
||||
let file_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::File)
|
||||
.collect();
|
||||
assert_eq!(file_nodes.len(), 1);
|
||||
assert_eq!(file_nodes[0].language, "rust");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_functions() {
|
||||
let source = "fn foo() {}\nfn bar() {}\nfn baz() {}\n";
|
||||
let output = parse_rust(source);
|
||||
let fn_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Function)
|
||||
.collect();
|
||||
assert_eq!(fn_nodes.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_main_is_entry_point() {
|
||||
let output = parse_rust("fn main() {\n println!(\"hi\");\n}\n");
|
||||
let main_node = output.nodes.iter().find(|n| n.name == "main").unwrap();
|
||||
assert!(main_node.is_entry_point);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pub_fn_is_entry_point() {
|
||||
let output = parse_rust("pub fn handler() {}\n");
|
||||
let node = output.nodes.iter().find(|n| n.name == "handler").unwrap();
|
||||
assert!(node.is_entry_point);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_private_fn_is_not_entry_point() {
|
||||
let output = parse_rust("fn helper() {}\n");
|
||||
let node = output.nodes.iter().find(|n| n.name == "helper").unwrap();
|
||||
assert!(!node.is_entry_point);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_function_calls_create_edges() {
|
||||
let source = "fn caller() {\n callee();\n}\nfn callee() {}\n";
|
||||
let output = parse_rust(source);
|
||||
let call_edges: Vec<_> = output
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|e| e.kind == CodeEdgeKind::Calls)
|
||||
.collect();
|
||||
assert!(!call_edges.is_empty());
|
||||
assert!(call_edges.iter().any(|e| e.target.contains("callee")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_use_declaration_creates_import_edge() {
|
||||
let source = "use std::collections::HashMap;\nfn foo() {}\n";
|
||||
let output = parse_rust(source);
|
||||
let import_edges: Vec<_> = output
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|e| e.kind == CodeEdgeKind::Imports)
|
||||
.collect();
|
||||
assert!(!import_edges.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_impl_methods() {
|
||||
let source = "struct Foo {}\nimpl Foo {\n fn do_thing(&self) {}\n}\n";
|
||||
let output = parse_rust(source);
|
||||
let fn_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Function)
|
||||
.collect();
|
||||
assert_eq!(fn_nodes.len(), 1);
|
||||
assert_eq!(fn_nodes[0].name, "do_thing");
|
||||
// Method should be qualified under the impl type
|
||||
assert!(fn_nodes[0].qualified_name.contains("Foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mod_item() {
|
||||
let source = "mod inner {\n fn nested() {}\n}\n";
|
||||
let output = parse_rust(source);
|
||||
let mod_nodes: Vec<_> = output
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.kind == CodeNodeKind::Module)
|
||||
.collect();
|
||||
assert_eq!(mod_nodes.len(), 1);
|
||||
assert_eq!(mod_nodes[0].name, "inner");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_line_numbers() {
|
||||
let source = "fn first() {}\n\n\nfn second() {}\n";
|
||||
let output = parse_rust(source);
|
||||
let first = output.nodes.iter().find(|n| n.name == "first").unwrap();
|
||||
let second = output.nodes.iter().find(|n| n.name == "second").unwrap();
|
||||
assert_eq!(first.start_line, 1);
|
||||
assert!(second.start_line > first.start_line);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_and_extensions() {
|
||||
let parser = RustParser::new();
|
||||
assert_eq!(parser.language(), "rust");
|
||||
assert_eq!(parser.extensions(), &["rs"]);
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageParser for RustParser {
|
||||
fn language(&self) -> &str {
|
||||
"rust"
|
||||
|
||||
@@ -128,3 +128,186 @@ impl SymbolIndex {
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use compliance_core::models::graph::CodeNodeKind;
|
||||
|
||||
fn make_node(
|
||||
qualified_name: &str,
|
||||
name: &str,
|
||||
kind: CodeNodeKind,
|
||||
file_path: &str,
|
||||
language: &str,
|
||||
) -> CodeNode {
|
||||
CodeNode {
|
||||
id: None,
|
||||
repo_id: "test".to_string(),
|
||||
graph_build_id: "build1".to_string(),
|
||||
qualified_name: qualified_name.to_string(),
|
||||
name: name.to_string(),
|
||||
kind,
|
||||
file_path: file_path.to_string(),
|
||||
start_line: 1,
|
||||
end_line: 10,
|
||||
language: language.to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_creates_index() {
|
||||
let index = SymbolIndex::new();
|
||||
assert!(index.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_empty_nodes() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let result = index.index_nodes(&[]);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_and_search_single_node() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let nodes = vec![make_node(
|
||||
"src/main.rs::main",
|
||||
"main",
|
||||
CodeNodeKind::Function,
|
||||
"src/main.rs",
|
||||
"rust",
|
||||
)];
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
let results = index.search("main", 10).unwrap();
|
||||
assert!(!results.is_empty());
|
||||
assert_eq!(results[0].name, "main");
|
||||
assert_eq!(results[0].qualified_name, "src/main.rs::main");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_no_results() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let nodes = vec![make_node(
|
||||
"src/main.rs::foo",
|
||||
"foo",
|
||||
CodeNodeKind::Function,
|
||||
"src/main.rs",
|
||||
"rust",
|
||||
)];
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
let results = index.search("zzzznonexistent", 10).unwrap();
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_multiple_nodes() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let nodes = vec![
|
||||
make_node(
|
||||
"a.rs::handle_request",
|
||||
"handle_request",
|
||||
CodeNodeKind::Function,
|
||||
"a.rs",
|
||||
"rust",
|
||||
),
|
||||
make_node(
|
||||
"b.rs::handle_response",
|
||||
"handle_response",
|
||||
CodeNodeKind::Function,
|
||||
"b.rs",
|
||||
"rust",
|
||||
),
|
||||
make_node(
|
||||
"c.rs::process_data",
|
||||
"process_data",
|
||||
CodeNodeKind::Function,
|
||||
"c.rs",
|
||||
"rust",
|
||||
),
|
||||
];
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
let results = index.search("handle", 10).unwrap();
|
||||
assert!(results.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_limit() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let mut nodes = Vec::new();
|
||||
for i in 0..20 {
|
||||
nodes.push(make_node(
|
||||
&format!("mod::func_{i}"),
|
||||
&format!("func_{i}"),
|
||||
CodeNodeKind::Function,
|
||||
"mod.rs",
|
||||
"rust",
|
||||
));
|
||||
}
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
let results = index.search("func", 5).unwrap();
|
||||
assert!(results.len() <= 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_has_score() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let nodes = vec![make_node(
|
||||
"src/lib.rs::compute",
|
||||
"compute",
|
||||
CodeNodeKind::Function,
|
||||
"src/lib.rs",
|
||||
"rust",
|
||||
)];
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
let results = index.search("compute", 10).unwrap();
|
||||
assert!(!results.is_empty());
|
||||
assert!(results[0].score > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_result_fields() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let nodes = vec![make_node(
|
||||
"src/app.py::MyClass",
|
||||
"MyClass",
|
||||
CodeNodeKind::Class,
|
||||
"src/app.py",
|
||||
"python",
|
||||
)];
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
let results = index.search("MyClass", 10).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].name, "MyClass");
|
||||
assert_eq!(results[0].kind, "class");
|
||||
assert_eq!(results[0].file_path, "src/app.py");
|
||||
assert_eq!(results[0].language, "python");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_empty_query() {
|
||||
let index = SymbolIndex::new().unwrap();
|
||||
let nodes = vec![make_node(
|
||||
"src/lib.rs::foo",
|
||||
"foo",
|
||||
CodeNodeKind::Function,
|
||||
"src/lib.rs",
|
||||
"rust",
|
||||
)];
|
||||
index.index_nodes(&nodes).unwrap();
|
||||
|
||||
// Empty query may parse error or return empty - both acceptable
|
||||
let result = index.search("", 10);
|
||||
// Just verify it doesn't panic
|
||||
let _ = result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user