use code_repo_wiki::analysis::graph::build;
use code_repo_wiki::ingest::parser::{Entity, FileInsight, ImportStmt};
use code_repo_wiki::model::{EdgeKind, KnowledgeGraph, NodeKind, NodeId};
use std::path::PathBuf;
use petgraph::visit::EdgeRef;
fn fn_entity(name: &str, line_start: usize, line_end: usize) -> Entity {
Entity {
name: name.into(),
kind: "fn".into(),
line_start,
line_end,
doc_comment: None,
signature: None,
visibility: None,
}
}
fn file_insight(path: &str, entities: Vec<Entity>, imports: &[ImportStmt], source: &str) -> FileInsight {
FileInsight {
path: PathBuf::from(path),
language: "rust".into(),
entities,
imports: imports.to_vec(),
doc_comments: vec![],
source: source.into(),
}
}
fn count_edges_of_kind(kg: &KnowledgeGraph, kind: EdgeKind) -> usize {
kg.graph
.edge_indices()
.filter(|&e| kg.graph.edge_weight(e).is_some_and(|w| w.kind == kind))
.count()
}
fn find_node(kg: &KnowledgeGraph, name: &str) -> NodeId {
kg.graph
.node_indices()
.find(|&n| kg.graph[n].name == name)
.unwrap_or_else(|| panic!("图中必须存在名为 {name} 的节点"))
}
#[test]
fn multi_name_collision_call_edges() {
let insights = vec![
file_insight("src/a.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}"),
file_insight("src/b.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}"),
file_insight(
"src/c.rs",
vec![fn_entity("caller", 1, 3)],
&[],
"fn caller() {\n helper(9);\n}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(kg.graph.node_count(), 8, "索引/节点同步:节点总数必须精确");
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Calls), 2, "同名两个实体都应收到调用边");
let helper_ids: Vec<NodeId> = kg
.graph
.node_indices()
.filter(|&n| kg.graph[n].name == "helper")
.collect();
assert_eq!(helper_ids.len(), 2);
let caller_id = find_node(&kg, "caller");
for h in &helper_ids {
assert_eq!(
kg.graph.edges_connecting(caller_id, *h).count(),
1,
"caller 应对每个同名 helper 各有一条调用边"
);
}
for e in kg
.graph
.edge_indices()
.filter(|&e| kg.graph.edge_weight(e).is_some_and(|w| w.kind == EdgeKind::Calls))
{
let t = kg.graph.edge_endpoints(e).unwrap().1;
assert_eq!(kg.graph[t].name, "helper", "调用边目标不得串到其他实体");
}
}
#[test]
fn multi_name_collision_import_edges() {
let insights = vec![
file_insight("src/a.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}"),
file_insight("src/b.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}"),
file_insight(
"src/c.rs",
vec![fn_entity("run", 1, 1)],
&[ImportStmt { source: "crate::a::helper".into(), alias: None, line: 1 }],
"fn run() {}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Imports), 2, "重名目标应各建一条 import 边");
for e in kg
.graph
.edge_indices()
.filter(|&e| kg.graph.edge_weight(e).is_some_and(|w| w.kind == EdgeKind::Imports))
{
let t = kg.graph.edge_endpoints(e).unwrap().1;
assert_eq!(kg.graph[t].name, "helper", "import 边目标不得串到其他实体");
}
}
#[test]
fn import_path_suffix_fallback() {
let insights = vec![
file_insight(
"crate/utils/helper.rs",
vec![fn_entity("do_thing", 1, 1)],
&[],
"fn do_thing() {}",
),
file_insight(
"src/main.rs",
vec![fn_entity("run", 1, 1)],
&[ImportStmt { source: "crate::utils::helper".into(), alias: None, line: 1 }],
"fn run() {}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Imports), 1, "路径后缀回退应命中 helper.rs 的实体");
let run_id = find_node(&kg, "run");
let do_thing_id = find_node(&kg, "do_thing");
let edge = kg
.graph
.edges_connecting(run_id, do_thing_id)
.next()
.expect("回退边必须存在");
assert_eq!(edge.weight().kind, EdgeKind::Imports);
assert_eq!(
kg.graph[edge.target()].kind,
NodeKind::Function,
"回退目标必须是实体节点而非文件/模块节点"
);
}
#[test]
fn import_name_match_precedes_path_fallback() {
let insights = vec![
file_insight("src/a.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}"),
file_insight(
"src/b.rs",
vec![fn_entity("run", 1, 1)],
&[ImportStmt { source: "crate::nonexistent::helper".into(), alias: None, line: 1 }],
"fn run() {}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Imports), 1, "name 命中即建边,不回退");
let run_id = find_node(&kg, "run");
let helper_id = find_node(&kg, "helper");
assert_eq!(kg.graph.edges_connecting(run_id, helper_id).count(), 1);
}
#[test]
fn call_dedup_same_body_via_build() {
let insights = vec![file_insight(
"src/lib.rs",
vec![fn_entity("helper", 1, 1), fn_entity("caller", 2, 6)],
&[],
"fn helper() {}\nfn caller() {\n helper(1);\n helper(2);\n helper(3);\n}",
)];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Calls), 1, "同一函数体内重复调用只建一条边");
}
#[test]
fn no_call_zero_edges_via_build() {
let insights = vec![file_insight(
"src/lib.rs",
vec![fn_entity("solo", 1, 3)],
&[],
"fn solo() {\n let x = 1;\n}",
)];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Calls), 0, "无调用应零 Calls 边");
}
#[test]
fn cross_file_call_index_sync() {
let insights = vec![
file_insight(
"src/caller.rs",
vec![fn_entity("caller", 1, 3)],
&[],
"fn caller() {\n callee(9);\n}",
),
file_insight("src/callee.rs", vec![fn_entity("callee", 1, 1)], &[], "fn callee() {}"),
];
let kg = build(&insights).unwrap();
assert_eq!(
count_edges_of_kind(&kg, EdgeKind::Calls),
1,
"后处理文件的符号必须可被调用解析(索引无漏插)"
);
let caller = find_node(&kg, "caller");
let callee = find_node(&kg, "callee");
assert_eq!(kg.graph.edges_connecting(caller, callee).count(), 1);
}
#[test]
fn import_resolves_only_processed_files() {
let insights = vec![
file_insight(
"src/f1.rs",
vec![fn_entity("use_early", 1, 1)],
&[ImportStmt { source: "later_helper".into(), alias: None, line: 1 }],
"fn use_early() {}",
),
file_insight("src/f2.rs", vec![fn_entity("later_helper", 1, 1)], &[], "fn later_helper() {}"),
file_insight(
"src/f3.rs",
vec![fn_entity("use_late", 1, 1)],
&[ImportStmt { source: "later_helper".into(), alias: None, line: 1 }],
"fn use_late() {}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Imports), 1, "只有反向(目标已处理)import 建边");
let use_early = find_node(&kg, "use_early");
let use_late = find_node(&kg, "use_late");
let later = find_node(&kg, "later_helper");
assert_eq!(kg.graph.edges_connecting(use_early, later).count(), 0, "前向 import 不建边");
assert_eq!(kg.graph.edges_connecting(use_late, later).count(), 1, "反向 import 建边");
}
#[test]
fn impl_edge_via_shared_index() {
let insights = vec![
file_insight(
"src/trait_def.rs",
vec![Entity {
name: "Greeter".into(),
kind: "trait".into(),
line_start: 1,
line_end: 1,
doc_comment: None,
signature: None,
visibility: None,
}],
&[],
"trait Greeter {}",
),
file_insight(
"src/impl_def.rs",
vec![Entity {
name: "impl Greeter for Foo".into(),
kind: "impl".into(),
line_start: 1,
line_end: 1,
doc_comment: None,
signature: None,
visibility: None,
}],
&[],
"impl Greeter for Foo {}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Implements), 1);
let impl_id = find_node(&kg, "impl Greeter for Foo");
let trait_id = find_node(&kg, "Greeter");
assert_eq!(kg.graph.edges_connecting(impl_id, trait_id).count(), 1);
}
#[test]
fn call_edges_crlf_source() {
let insights = vec![
file_insight("src/a.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}\r\n"),
file_insight(
"src/b.rs",
vec![fn_entity("caller", 1, 3)],
&[],
"fn caller() {\r\n helper(7);\r\n}\r\n",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Calls), 1, "CRLF 行尾不应破坏调用边解析");
}
#[test]
fn call_edges_paren_adjacency() {
let insights = vec![
file_insight("src/a.rs", vec![fn_entity("helper", 1, 1)], &[], "fn helper() {}"),
file_insight(
"src/b.rs",
vec![fn_entity("caller", 1, 3)],
&[],
"fn caller() {\n helper (1);\n}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Calls), 0, "helper 与 ( 之间有空格不算调用");
}
#[test]
fn node_count_index_sync() {
let insights = vec![
file_insight("src/a.rs", vec![fn_entity("fa", 1, 1)], &[], "fn fa() {}"),
file_insight("src/b.rs", vec![fn_entity("fb", 1, 1)], &[], "fn fb() {}"),
file_insight(
"util/c.rs",
vec![fn_entity("fc", 1, 1), fn_entity("fd", 2, 2)],
&[],
"fn fc() {}\nfn fd() {}",
),
];
let kg = build(&insights).unwrap();
assert_eq!(kg.graph.node_count(), 10, "1 project + 2 模块 + 3 文件 + 4 实体");
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Contains), 9);
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Calls), 0);
assert_eq!(count_edges_of_kind(&kg, EdgeKind::Imports), 0);
for (name, expect_file) in [("fa", "a.rs"), ("fb", "b.rs"), ("fc", "c.rs"), ("fd", "c.rs")] {
let n = find_node(&kg, name);
assert_eq!(kg.graph[n].kind, NodeKind::Function);
let file = kg.graph[n].file_path.as_deref().expect("实体必须归属文件");
assert_eq!(
file.rsplit(['/', '\\']).next().unwrap(),
expect_file,
"实体 file_path 必须归属正确文件"
);
}
}