use std::any::TypeId;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use crate::kit::{AsyncAutoBuilder, AsyncKit, ModuleMeta};
use crate::model::EdgeType;
use crate::storage::StorageError;
use super::capability::TraceEngine;
use super::error::TraceError;
use super::facade::{PathFilter, TraceFacade, TraceType};
use super::graph_loader::load_graph_for_symbol;
use super::TraceResult;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceConfig {
pub db_path: PathBuf,
pub max_depth: u32,
pub edge_types: Vec<EdgeType>,
pub path_filter: Option<PathFilter>,
pub detect_cycles: bool,
pub cross_service: bool,
}
const MAX_DEPTH_LIMIT: u32 = 10;
impl Default for TraceConfig {
fn default() -> Self {
Self {
db_path: PathBuf::from(":memory:"),
max_depth: 5,
edge_types: vec![EdgeType::Calls],
path_filter: None,
detect_cycles: false,
cross_service: false,
}
}
}
impl TraceConfig {
#[must_use]
pub fn in_memory() -> Self {
Self::default()
}
#[must_use]
pub fn clamped_depth(&self) -> u32 {
self.max_depth.min(MAX_DEPTH_LIMIT)
}
}
pub struct TraceModule;
impl ModuleMeta for TraceModule {
const NAME: &'static str = "trace";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for TraceModule {
type Capability = Arc<dyn TraceEngine>;
type Error = TraceError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let config = kit
.config::<TraceConfig>()
.map_err(|e| TraceError::Storage(StorageError::InvalidData(e.to_string())))?;
Self::build_cap(&config)
})
}
}
impl TraceModule {
pub(crate) fn build_cap(config: &TraceConfig) -> Result<Arc<dyn TraceEngine>, TraceError> {
Ok(Arc::new(TraceCapability {
db_path: config.db_path.clone(),
}))
}
}
struct TraceCapability {
db_path: PathBuf,
}
impl TraceEngine for TraceCapability {
fn trace(
&self,
symbol: &str,
trace_type: TraceType,
depth: usize,
) -> std::result::Result<TraceResult, TraceError> {
let graph = load_graph_for_symbol(&self.db_path, symbol, depth)?;
let facade = TraceFacade::new(&graph);
facade.trace(symbol, trace_type, depth)
}
fn load_graph(
&self,
symbol: &str,
depth: usize,
) -> std::result::Result<crate::model::Graph, TraceError> {
Ok(load_graph_for_symbol(&self.db_path, symbol, depth)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kit::{AsyncKit, TraceModule};
use crate::storage::StorageConnection;
use tempfile::TempDir;
fn fresh_db_path() -> std::path::PathBuf {
let dir = TempDir::new().unwrap();
let path = dir.path().join("trace_module_testdb");
std::mem::forget(dir);
path
}
fn seed_call_graph(db: &std::path::Path) {
let conn = StorageConnection::open(db).expect("open");
conn.init_schema().expect("init_schema");
conn.execute("CREATE (:Function {id: 'f_a', project: 'demo', name: 'a', qualifiedName: 'demo.a', filePath: '/src/a.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create a");
conn.execute("CREATE (:Function {id: 'f_b', project: 'demo', name: 'b', qualifiedName: 'demo.b', filePath: '/src/b.rs', startLine: 1, endLine: 5, signature: '', returnType: '', isExported: false, docstring: '', content: '', parentQn: ''});").expect("create b");
conn.execute("CREATE (:CodeRelation {id: 'e1', source: 'f_a', target: 'f_b', type: 'CALLS', confidence: 1.0, reason: 'direct call', startLine: 2, project: 'demo'});").expect("create edge");
}
fn build_trace_seeded() -> (Arc<dyn TraceEngine>, std::path::PathBuf) {
let db = fresh_db_path();
seed_call_graph(&db);
let cap = TraceModule::build_cap(&TraceConfig {
db_path: db.clone(),
..Default::default()
})
.expect("TraceModule::build_cap");
(cap, db)
}
#[test]
fn build_returns_send_sync_capability() {
let (cap, _db) = build_trace_seeded();
fn _assert_send_sync<T: Send + Sync>(_: &T) {}
_assert_send_sync(&cap);
}
#[test]
fn capability_trace_calls_returns_path() {
let (cap, _db) = build_trace_seeded();
let result = cap.trace("a", TraceType::Calls, 3).expect("trace");
assert_eq!(result.symbol, "a");
assert_eq!(result.paths.len(), 1, "should have 1 call path a->b");
let names: Vec<&str> = result.paths[0]
.nodes
.iter()
.map(|n| n.name.as_str())
.collect();
assert_eq!(names, vec!["a", "b"]);
assert_eq!(result.paths[0].edges[0].edge_type, "CALLS");
}
#[test]
fn capability_trace_symbol_not_found_returns_error() {
let (cap, _db) = build_trace_seeded();
let result = cap.trace("missing", TraceType::Calls, 3);
assert!(
matches!(result, Err(TraceError::SymbolNotFound(_))),
"got {result:?}"
);
}
#[test]
fn capability_trace_invalid_depth_returns_error() {
let (cap, _db) = build_trace_seeded();
let result = cap.trace("a", TraceType::Calls, 0);
assert!(matches!(result, Err(TraceError::InvalidDepth(0))));
}
#[test]
fn capability_trace_missing_db_returns_storage_error() {
let cap = TraceModule::build_cap(&TraceConfig {
db_path: std::path::PathBuf::from("/nonexistent/db.lbug"),
..Default::default()
})
.expect("build_cap");
let result = cap.trace("a", TraceType::Calls, 3);
assert!(
matches!(result, Err(TraceError::Storage(_))),
"missing db → Storage error, got {result:?}"
);
}
#[tokio::test]
async fn kit_registration_flow() {
let db = fresh_db_path();
seed_call_graph(&db);
let mut kit = AsyncKit::new();
kit.set_config(TraceConfig {
db_path: db.clone(),
..Default::default()
});
kit.register::<TraceModule>()
.expect("register::<TraceModule>");
let kit = kit.build().await.expect("build");
assert!(kit.contains::<TraceModule>(), "TraceModule missing");
let required = kit
.require::<TraceModule>()
.expect("require::<TraceModule>");
let result = required.trace("a", TraceType::Calls, 3).expect("trace");
assert_eq!(result.symbol, "a");
}
#[test]
fn trace_config_in_memory_sets_memory_path() {
let config = TraceConfig::in_memory();
assert_eq!(config.db_path, std::path::PathBuf::from(":memory:"));
}
}