use std::path::PathBuf;
use dk_core::{
CallEdge, CallKind, Dependency, Span, Symbol, SymbolKind, TypeInfo, Visibility,
};
use dk_engine::graph::{CallGraphStore, DependencyStore, SymbolStore, TypeInfoStore};
use sqlx::PgPool;
use uuid::Uuid;
async fn setup_pool() -> PgPool {
let url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://localhost/dkod_test".to_string());
let pool = PgPool::connect(&url)
.await
.expect("Failed to connect to test database");
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("Failed to run migrations");
pool
}
fn make_symbol(name: &str, kind: SymbolKind, file: &str) -> Symbol {
Symbol {
id: Uuid::new_v4(),
name: name.to_string(),
qualified_name: format!("test_mod::{name}"),
kind,
visibility: Visibility::Public,
file_path: PathBuf::from(file),
span: Span {
start_byte: 0,
end_byte: 100,
},
signature: Some(format!("fn {name}()")),
doc_comment: None,
parent: None,
last_modified_by: None,
last_modified_intent: None,
}
}
async fn create_test_repo(pool: &PgPool) -> Uuid {
let repo_id = Uuid::new_v4();
sqlx::query("INSERT INTO repositories (id, name, path) VALUES ($1, $2, $3)")
.bind(repo_id)
.bind(format!("test-repo-{repo_id}"))
.bind("/tmp/test")
.execute(pool)
.await
.expect("Failed to create test repo");
repo_id
}
async fn cleanup_repo(pool: &PgPool, repo_id: Uuid) {
sqlx::query("DELETE FROM repositories WHERE id = $1")
.bind(repo_id)
.execute(pool)
.await
.expect("Failed to clean up test repo");
}
#[tokio::test]
async fn test_insert_edge_and_find_callees() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let cg_store = CallGraphStore::new(pool.clone());
let caller = make_symbol("main", SymbolKind::Function, "src/main.rs");
let callee = make_symbol("helper", SymbolKind::Function, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &caller).await.unwrap();
sym_store.upsert_symbol(repo_id, &callee).await.unwrap();
let edge = CallEdge {
id: Uuid::new_v4(),
repo_id,
caller: caller.id,
callee: callee.id,
kind: CallKind::DirectCall,
};
cg_store.insert_edge(&edge).await.unwrap();
let callees = cg_store.find_callees(caller.id).await.unwrap();
assert_eq!(callees.len(), 1);
assert_eq!(callees[0].callee, callee.id);
assert_eq!(callees[0].kind, CallKind::DirectCall);
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_find_callers() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let cg_store = CallGraphStore::new(pool.clone());
let caller_a = make_symbol("func_a", SymbolKind::Function, "src/a.rs");
let caller_b = make_symbol("func_b", SymbolKind::Function, "src/b.rs");
let target = make_symbol("target", SymbolKind::Function, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &caller_a).await.unwrap();
sym_store.upsert_symbol(repo_id, &caller_b).await.unwrap();
sym_store.upsert_symbol(repo_id, &target).await.unwrap();
let edge_a = CallEdge {
id: Uuid::new_v4(),
repo_id,
caller: caller_a.id,
callee: target.id,
kind: CallKind::DirectCall,
};
let edge_b = CallEdge {
id: Uuid::new_v4(),
repo_id,
caller: caller_b.id,
callee: target.id,
kind: CallKind::MethodCall,
};
cg_store.insert_edge(&edge_a).await.unwrap();
cg_store.insert_edge(&edge_b).await.unwrap();
let callers = cg_store.find_callers(target.id).await.unwrap();
assert_eq!(callers.len(), 2);
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_insert_edge_idempotent() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let cg_store = CallGraphStore::new(pool.clone());
let caller = make_symbol("caller_idem", SymbolKind::Function, "src/main.rs");
let callee = make_symbol("callee_idem", SymbolKind::Function, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &caller).await.unwrap();
sym_store.upsert_symbol(repo_id, &callee).await.unwrap();
let edge = CallEdge {
id: Uuid::new_v4(),
repo_id,
caller: caller.id,
callee: callee.id,
kind: CallKind::Import,
};
cg_store.insert_edge(&edge).await.unwrap();
cg_store.insert_edge(&edge).await.unwrap();
let callees = cg_store.find_callees(caller.id).await.unwrap();
assert_eq!(callees.len(), 1);
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_delete_edges_for_file() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let cg_store = CallGraphStore::new(pool.clone());
let caller_in_file = make_symbol("in_file", SymbolKind::Function, "src/old.rs");
let caller_other = make_symbol("other_file", SymbolKind::Function, "src/keep.rs");
let callee = make_symbol("target_del", SymbolKind::Function, "src/lib.rs");
sym_store
.upsert_symbol(repo_id, &caller_in_file)
.await
.unwrap();
sym_store
.upsert_symbol(repo_id, &caller_other)
.await
.unwrap();
sym_store.upsert_symbol(repo_id, &callee).await.unwrap();
let edge1 = CallEdge {
id: Uuid::new_v4(),
repo_id,
caller: caller_in_file.id,
callee: callee.id,
kind: CallKind::DirectCall,
};
let edge2 = CallEdge {
id: Uuid::new_v4(),
repo_id,
caller: caller_other.id,
callee: callee.id,
kind: CallKind::DirectCall,
};
cg_store.insert_edge(&edge1).await.unwrap();
cg_store.insert_edge(&edge2).await.unwrap();
let deleted = cg_store
.delete_edges_for_file(repo_id, "src/old.rs")
.await
.unwrap();
assert_eq!(deleted, 1);
let callers = cg_store.find_callers(callee.id).await.unwrap();
assert_eq!(callers.len(), 1);
assert_eq!(callers[0].caller, caller_other.id);
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_upsert_and_find_dependencies() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let dep_store = DependencyStore::new(pool.clone());
let dep = Dependency {
id: Uuid::new_v4(),
repo_id,
package: "serde".to_string(),
version_req: "^1.0".to_string(),
};
dep_store.upsert_dependency(&dep).await.unwrap();
let deps = dep_store.find_by_repo(repo_id).await.unwrap();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].package, "serde");
assert_eq!(deps[0].version_req, "^1.0");
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_upsert_dependency_updates_version() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let dep_store = DependencyStore::new(pool.clone());
let dep = Dependency {
id: Uuid::new_v4(),
repo_id,
package: "tokio".to_string(),
version_req: "^1.0".to_string(),
};
dep_store.upsert_dependency(&dep).await.unwrap();
let dep2 = Dependency {
id: Uuid::new_v4(),
repo_id,
package: "tokio".to_string(),
version_req: "^1.38".to_string(),
};
dep_store.upsert_dependency(&dep2).await.unwrap();
let deps = dep_store.find_by_repo(repo_id).await.unwrap();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].version_req, "^1.38");
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_link_symbol_to_dep() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let dep_store = DependencyStore::new(pool.clone());
let sym = make_symbol("use_serde", SymbolKind::Function, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &sym).await.unwrap();
let dep = Dependency {
id: Uuid::new_v4(),
repo_id,
package: "serde".to_string(),
version_req: "^1.0".to_string(),
};
dep_store.upsert_dependency(&dep).await.unwrap();
dep_store
.link_symbol_to_dep(sym.id, dep.id)
.await
.unwrap();
dep_store
.link_symbol_to_dep(sym.id, dep.id)
.await
.unwrap();
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_delete_dependencies_by_repo() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let dep_store = DependencyStore::new(pool.clone());
let dep1 = Dependency {
id: Uuid::new_v4(),
repo_id,
package: "anyhow".to_string(),
version_req: "^1".to_string(),
};
let dep2 = Dependency {
id: Uuid::new_v4(),
repo_id,
package: "thiserror".to_string(),
version_req: "^2".to_string(),
};
dep_store.upsert_dependency(&dep1).await.unwrap();
dep_store.upsert_dependency(&dep2).await.unwrap();
let deleted = dep_store.delete_by_repo(repo_id).await.unwrap();
assert_eq!(deleted, 2);
let deps = dep_store.find_by_repo(repo_id).await.unwrap();
assert!(deps.is_empty());
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_upsert_and_get_type_info() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let ti_store = TypeInfoStore::new(pool.clone());
let sym = make_symbol("MyStruct", SymbolKind::Struct, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &sym).await.unwrap();
let info = TypeInfo {
symbol_id: sym.id,
params: vec![
("T".to_string(), "Clone".to_string()),
("U".to_string(), "Debug".to_string()),
],
return_type: None,
fields: vec![
("name".to_string(), "String".to_string()),
("age".to_string(), "u32".to_string()),
],
implements: vec!["Clone".to_string(), "Debug".to_string()],
};
ti_store.upsert_type_info(&info).await.unwrap();
let fetched = ti_store.get_by_symbol_id(sym.id).await.unwrap();
assert!(fetched.is_some());
let fetched = fetched.unwrap();
assert_eq!(fetched.symbol_id, sym.id);
assert_eq!(fetched.params.len(), 2);
assert_eq!(fetched.params[0], ("T".to_string(), "Clone".to_string()));
assert_eq!(fetched.params[1], ("U".to_string(), "Debug".to_string()));
assert!(fetched.return_type.is_none());
assert_eq!(fetched.fields.len(), 2);
assert_eq!(fetched.fields[0], ("name".to_string(), "String".to_string()));
assert_eq!(fetched.implements, vec!["Clone", "Debug"]);
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_upsert_type_info_updates() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let ti_store = TypeInfoStore::new(pool.clone());
let sym = make_symbol("my_func_ti", SymbolKind::Function, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &sym).await.unwrap();
let info1 = TypeInfo {
symbol_id: sym.id,
params: vec![("x".to_string(), "i32".to_string())],
return_type: Some("bool".to_string()),
fields: vec![],
implements: vec![],
};
ti_store.upsert_type_info(&info1).await.unwrap();
let info2 = TypeInfo {
symbol_id: sym.id,
params: vec![
("x".to_string(), "i32".to_string()),
("y".to_string(), "i32".to_string()),
],
return_type: Some("i64".to_string()),
fields: vec![],
implements: vec![],
};
ti_store.upsert_type_info(&info2).await.unwrap();
let fetched = ti_store.get_by_symbol_id(sym.id).await.unwrap().unwrap();
assert_eq!(fetched.params.len(), 2);
assert_eq!(fetched.return_type.as_deref(), Some("i64"));
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_delete_type_info() {
let pool = setup_pool().await;
let repo_id = create_test_repo(&pool).await;
let sym_store = SymbolStore::new(pool.clone());
let ti_store = TypeInfoStore::new(pool.clone());
let sym = make_symbol("to_delete_ti", SymbolKind::Struct, "src/lib.rs");
sym_store.upsert_symbol(repo_id, &sym).await.unwrap();
let info = TypeInfo {
symbol_id: sym.id,
params: vec![],
return_type: None,
fields: vec![("x".to_string(), "f64".to_string())],
implements: vec!["Display".to_string()],
};
ti_store.upsert_type_info(&info).await.unwrap();
let deleted = ti_store.delete_by_symbol_id(sym.id).await.unwrap();
assert_eq!(deleted, 1);
let fetched = ti_store.get_by_symbol_id(sym.id).await.unwrap();
assert!(fetched.is_none());
cleanup_repo(&pool, repo_id).await;
}
#[tokio::test]
async fn test_get_type_info_not_found() {
let pool = setup_pool().await;
let ti_store = TypeInfoStore::new(pool.clone());
let result = ti_store.get_by_symbol_id(Uuid::new_v4()).await.unwrap();
assert!(result.is_none());
}