use std::collections::HashMap;
use std::fs;
use tempfile::tempdir;
use codesearch::symbols::{
Symbol, SymbolIndex, SymbolKind, SymbolVisibility,
extractor::{SymbolExtractor, extract_symbols_from_file},
relationships::RelationshipGraph,
};
fn create_symbol_test_project() -> tempfile::TempDir {
let dir = tempdir().unwrap();
let src_dir = dir.path().join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("main.rs"),
r#"// Main entry point
pub struct Config {
name: String,
value: i32,
}
impl Config {
pub fn new(name: &str) -> Self {
Config { name: name.to_string(), value: 0 }
}
pub fn get_name(&self) -> &str {
&self.name
}
}
pub fn process_config(config: &Config) -> String {
format!("Config: {}", config.get_name())
}
fn main() {
let config = Config::new("test");
println!("{}", process_config(&config));
}
"#,
)
.unwrap();
let py_dir = dir.path().join("scripts");
fs::create_dir_all(&py_dir).unwrap();
fs::write(
py_dir.join("processor.py"),
r#"class DataProcessor:
def __init__(self, name):
self.name = name
def process(self, data):
return [x * 2 for x in data]
def main():
processor = DataProcessor("test")
result = processor.process([1, 2, 3])
print(result)
"#,
)
.unwrap();
let js_dir = dir.path().join("web");
fs::create_dir_all(&js_dir).unwrap();
fs::write(
js_dir.join("app.js"),
r#"class ApiHandler {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(endpoint) {
return fetch(`${this.baseUrl}${endpoint}`);
}
}
function createHandler(url) {
return new ApiHandler(url);
}
"#,
)
.unwrap();
dir
}
#[test]
fn test_extract_symbols_from_rust() {
let dir = create_symbol_test_project();
let file_path = dir.path().join("src/main.rs");
let symbols = extract_symbols_from_file(&file_path).unwrap();
assert!(!symbols.is_empty(), "Should extract symbols from Rust file");
let has_struct = symbols.iter().any(|s| s.kind == SymbolKind::Struct);
assert!(has_struct, "Should find struct definition");
let has_function = symbols.iter().any(|s| s.kind == SymbolKind::Function);
assert!(has_function, "Should find function definitions");
}
#[test]
fn test_extract_symbols_from_python() {
let dir = create_symbol_test_project();
let file_path = dir.path().join("scripts/processor.py");
let symbols = extract_symbols_from_file(&file_path).unwrap();
assert!(
!symbols.is_empty(),
"Should extract symbols from Python file"
);
let has_class = symbols.iter().any(|s| s.kind == SymbolKind::Class);
assert!(has_class, "Should find class definition");
}
#[test]
fn test_extract_symbols_from_js() {
let dir = create_symbol_test_project();
let file_path = dir.path().join("web/app.js");
let symbols = extract_symbols_from_file(&file_path).unwrap();
assert!(!symbols.is_empty(), "Should extract symbols from JS file");
let has_class = symbols.iter().any(|s| s.kind == SymbolKind::Class);
assert!(has_class, "Should find class definition in JS");
}
#[test]
fn test_symbol_metadata() {
let dir = create_symbol_test_project();
let file_path = dir.path().join("src/main.rs");
let symbols = extract_symbols_from_file(&file_path).unwrap();
for symbol in &symbols {
assert!(!symbol.id.is_empty(), "Symbol should have an ID");
assert!(!symbol.name.is_empty(), "Symbol should have a name");
assert!(
!symbol.file_path.is_empty(),
"Symbol should have a file path"
);
assert!(symbol.line > 0, "Symbol should have a line number");
}
}
#[test]
fn test_symbol_index_basic() {
let index = SymbolIndex::new();
let symbol = Symbol {
id: "test_id".to_string(),
name: "test_func".to_string(),
kind: SymbolKind::Function,
file_path: "src/main.rs".to_string(),
line: 10,
column: 0,
end_line: 15,
signature: "()".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
index.add_symbol(symbol.clone());
let found = index.get_symbol("test_id");
assert!(found.is_some(), "Should find symbol by ID");
assert_eq!(found.unwrap().name, "test_func");
}
#[test]
fn test_symbol_index_find_by_name() {
let index = SymbolIndex::new();
let names = vec!["func_a", "func_b", "func_c"];
for (i, name) in names.iter().enumerate() {
let symbol = Symbol {
id: format!("id_{}", i),
name: name.to_string(),
kind: SymbolKind::Function,
file_path: "src/main.rs".to_string(),
line: i,
column: 0,
end_line: i + 5,
signature: "()".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
index.add_symbol(symbol);
}
let found = index.find_by_name("func_b");
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "func_b");
}
#[test]
fn test_symbol_index_search_by_kind() {
let dir = create_symbol_test_project();
let file_path = dir.path().join("src/main.rs");
let symbols = extract_symbols_from_file(&file_path).unwrap();
let index = SymbolIndex::new();
for symbol in symbols {
index.add_symbol(symbol);
}
let functions = index.get_symbols_by_kind(&SymbolKind::Function);
let structs = index.get_symbols_by_kind(&SymbolKind::Struct);
assert!(!functions.is_empty(), "Should find functions");
assert!(!structs.is_empty(), "Should find structs");
}
#[test]
fn test_symbol_index_get_symbols_in_file() {
let dir = create_symbol_test_project();
let index = SymbolIndex::new();
let rust_path = dir.path().join("src/main.rs");
let py_path = dir.path().join("scripts/processor.py");
let rust_symbols = extract_symbols_from_file(&rust_path).unwrap();
let py_symbols = extract_symbols_from_file(&py_path).unwrap();
for s in rust_symbols {
index.add_symbol(s);
}
for s in py_symbols {
index.add_symbol(s);
}
let rust_file_symbols = index.get_symbols_in_file(&rust_path.to_string_lossy().to_string());
let py_file_symbols = index.get_symbols_in_file(&py_path.to_string_lossy().to_string());
assert!(
!rust_file_symbols.is_empty(),
"Should find symbols in Rust file"
);
assert!(
!py_file_symbols.is_empty(),
"Should find symbols in Python file"
);
}
#[test]
fn test_symbol_index_stats() {
let dir = create_symbol_test_project();
let index = SymbolIndex::new();
let rust_path = dir.path().join("src/main.rs");
let symbols = extract_symbols_from_file(&rust_path).unwrap();
for s in symbols {
index.add_symbol(s);
}
let stats = index.get_stats();
assert!(stats.total_symbols > 0, "Should have symbols in stats");
assert!(stats.total_files > 0, "Should have files in stats");
}
#[test]
fn test_relationship_graph_basic() {
let graph = RelationshipGraph::new();
let sym_a = Symbol {
id: "sym_a".to_string(),
name: "function_a".to_string(),
kind: SymbolKind::Function,
file_path: "a.rs".to_string(),
line: 1,
column: 0,
end_line: 5,
signature: "()".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
let sym_b = Symbol {
id: "sym_b".to_string(),
name: "function_b".to_string(),
kind: SymbolKind::Function,
file_path: "b.rs".to_string(),
line: 10,
column: 0,
end_line: 15,
signature: "()".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
graph.add_symbol(sym_a.clone());
graph.add_symbol(sym_b.clone());
graph.add_relationship(
"sym_a",
"sym_b",
codesearch::symbols::SymbolRelationType::Calls,
1.0,
);
let relationships = graph.get_relationships("sym_a");
assert_eq!(relationships.len(), 1);
assert_eq!(relationships[0].symbol_id, "sym_b");
}
#[test]
fn test_relationship_graph_find_callers() {
let graph = RelationshipGraph::new();
for i in 0..5 {
let symbol = Symbol {
id: format!("sym_{}", i),
name: format!("func_{}", i),
kind: SymbolKind::Function,
file_path: "test.rs".to_string(),
line: i,
column: 0,
end_line: i + 5,
signature: "()".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
graph.add_symbol(symbol);
}
graph.add_relationship(
"sym_1",
"sym_0",
codesearch::symbols::SymbolRelationType::Calls,
1.0,
);
graph.add_relationship(
"sym_2",
"sym_0",
codesearch::symbols::SymbolRelationType::Calls,
1.0,
);
let callers = graph.find_callers("sym_0");
assert_eq!(callers.len(), 2, "Should find 2 callers");
}
#[test]
fn test_relationship_graph_hierarchy() {
let graph = RelationshipGraph::new();
let parent = Symbol {
id: "parent".to_string(),
name: "BaseClass".to_string(),
kind: SymbolKind::Class,
file_path: "test.rs".to_string(),
line: 1,
column: 0,
end_line: 10,
signature: "".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
let child = Symbol {
id: "child".to_string(),
name: "DerivedClass".to_string(),
kind: SymbolKind::Class,
file_path: "test.rs".to_string(),
line: 20,
column: 0,
end_line: 30,
signature: "".to_string(),
documentation: None,
visibility: SymbolVisibility::Public,
parent: None,
type_info: None,
generics: vec![],
annotations: vec![],
attributes: vec![],
metadata: HashMap::new(),
};
graph.add_symbol(parent);
graph.add_symbol(child);
graph.add_relationship(
"child",
"parent",
codesearch::symbols::SymbolRelationType::Inherits,
1.0,
);
let hierarchy = graph.find_hierarchy("parent");
assert!(
hierarchy.len() >= 2,
"Should find hierarchy with parent and child"
);
}
#[test]
fn test_symbol_index_store_directory_indexing() {
let dir = create_symbol_test_project();
let store_path = dir.path().join(".codesearch").join("symbol_index");
let store = codesearch::symbols::SymbolIndexStore::new(store_path);
let count = store
.index_directory(
dir.path(),
Some(&["rs".to_string(), "py".to_string(), "js".to_string()]),
None,
)
.unwrap();
assert!(count > 0, "Should index symbols from directory");
let stats = store.index().get_stats();
assert!(stats.total_symbols > 0, "Should have indexed symbols");
assert!(stats.total_files > 0, "Should have indexed files");
}
#[test]
fn test_symbol_end_to_end_workflow() {
let dir = create_symbol_test_project();
let index = SymbolIndex::new();
for entry in walkdir::WalkDir::new(dir.path())
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "rs" || ext == "py" || ext == "js" {
if let Ok(symbols) = extract_symbols_from_file(path) {
for symbol in symbols {
index.add_symbol(symbol);
}
}
}
}
}
}
let config_symbols = index.search_symbols(Some("Config"), None, None, None);
assert!(!config_symbols.is_empty(), "Should find Config symbols");
let functions = index.get_symbols_by_kind(&SymbolKind::Function);
assert!(!functions.is_empty(), "Should find functions");
let stats = index.get_stats();
assert!(stats.total_symbols > 0);
assert!(!stats.symbols_by_kind.is_empty());
}
#[cfg(feature = "mcp")]
mod mcp_tool_tests {
use super::*;
use codesearch::mcp::symbols_tools::*;
#[test]
fn test_search_symbols_params_serialization() {
let params = SearchSymbolsParams {
pattern: Some("test".to_string()),
path: Some("/project".to_string()),
extensions: Some(vec!["rs".to_string()]),
exclude: Some(vec!["target".to_string()]),
kind: Some(SymbolKind::Function),
file_path: None,
visibility: Some(SymbolVisibility::Public),
context_lines: Some(3),
include_related: Some(true),
limit: Some(10),
};
let json = serde_json::to_string(¶ms).unwrap();
let deserialized: SearchSymbolsParams = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.pattern, params.pattern);
assert_eq!(deserialized.extensions, params.extensions);
assert_eq!(deserialized.limit, params.limit);
}
#[test]
fn test_get_symbol_details_params_serialization() {
let params = GetSymbolDetailsParams {
id: Some("sym_123".to_string()),
name: Some("my_func".to_string()),
file_path: Some("src/main.rs".to_string()),
};
let json = serde_json::to_string(¶ms).unwrap();
let deserialized: GetSymbolDetailsParams = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, params.id);
assert_eq!(deserialized.name, params.name);
}
#[test]
fn test_build_symbol_index_params_serialization() {
let params = BuildSymbolIndexParams {
path: Some("/project".to_string()),
extensions: Some(vec!["rs".to_string(), "py".to_string()]),
exclude: Some(vec!["target".to_string()]),
};
let json = serde_json::to_string(¶ms).unwrap();
let deserialized: BuildSymbolIndexParams = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.path, params.path);
assert_eq!(deserialized.extensions, params.extensions);
}
#[test]
fn test_find_symbol_relationships_params_serialization() {
let params = FindSymbolRelationshipsParams {
symbol_id: Some("sym_1".to_string()),
symbol_name: None,
relation_types: Some(vec![
codesearch::symbols::SymbolRelationType::Calls,
codesearch::symbols::SymbolRelationType::Inherits,
]),
};
let json = serde_json::to_string(¶ms).unwrap();
let deserialized: FindSymbolRelationshipsParams = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.symbol_id, params.symbol_id);
assert!(deserialized.relation_types.is_some());
}
#[test]
fn test_find_symbol_hierarchy_params_serialization() {
let params = FindSymbolHierarchyParams {
symbol_id: Some("class_1".to_string()),
symbol_name: Some("MyClass".to_string()),
};
let json = serde_json::to_string(¶ms).unwrap();
let deserialized: FindSymbolHierarchyParams = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.symbol_id, params.symbol_id);
assert_eq!(deserialized.symbol_name, params.symbol_name);
}
#[tokio::test]
async fn test_build_symbol_index_tool() {
let dir = super::create_symbol_test_project();
let params = BuildSymbolIndexParams {
path: Some(dir.path().to_string_lossy().to_string()),
extensions: Some(vec!["rs".to_string()]),
exclude: None,
};
let result =
build_symbol_index_tool(rmcp::handler::server::wrapper::Parameters(params)).await;
let value = result.0;
assert!(value.get("success").unwrap().as_bool().unwrap());
assert!(value.get("indexed_symbols").unwrap().as_u64().unwrap() > 0);
}
#[tokio::test]
async fn test_get_index_stats_tool() {
let dir = super::create_symbol_test_project();
let build_params = BuildSymbolIndexParams {
path: Some(dir.path().to_string_lossy().to_string()),
extensions: Some(vec!["rs".to_string(), "py".to_string()]),
exclude: None,
};
let _ =
build_symbol_index_tool(rmcp::handler::server::wrapper::Parameters(build_params)).await;
let stats_params = GetIndexStatsParams {};
let result =
get_index_stats_tool(rmcp::handler::server::wrapper::Parameters(stats_params)).await;
let value = result.0;
assert!(value.get("total_symbols").unwrap().as_u64().unwrap() > 0);
assert!(value.get("total_files").unwrap().as_u64().unwrap() > 0);
}
}