use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use rustcodegraph::types::{
BuildContextOptions, ContextFormat, EdgeKind, SearchOptions, TaskInput,
};
use rustcodegraph::{BuildContextResult, CodeGraph, IndexOptions};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn create_temp_dir(prefix: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after Unix epoch")
.as_nanos();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = env::temp_dir().join(format!("{prefix}{}-{unique}-{counter}", std::process::id()));
fs::create_dir_all(&dir)
.unwrap_or_else(|err| panic!("failed to create temp dir {}: {err}", dir.display()));
dir
}
fn cleanup_temp_dir(dir: &Path) {
if dir.exists() {
let _ = fs::remove_dir_all(dir);
}
}
struct TempProject {
root: PathBuf,
}
impl TempProject {
fn new() -> Self {
Self {
root: create_temp_dir("codegraph-int-"),
}
}
fn path(&self) -> &Path {
&self.root
}
}
impl Drop for TempProject {
fn drop(&mut self) {
cleanup_temp_dir(&self.root);
}
}
struct CodeGraphGuard {
cg: CodeGraph,
}
impl CodeGraphGuard {
fn init(project_root: &Path) -> Self {
let cg = CodeGraph::init_sync(project_root).expect("CodeGraph should initialize");
Self { cg }
}
}
impl Drop for CodeGraphGuard {
fn drop(&mut self) {
self.cg.destroy();
}
}
fn write_file(path: impl AsRef<Path>, contents: &str) {
let path = path.as_ref();
fs::write(path, contents)
.unwrap_or_else(|err| panic!("failed to write fixture file {}: {err}", path.display()));
}
fn search_limit(limit: u64) -> Option<SearchOptions> {
Some(SearchOptions {
kinds: None,
languages: None,
include_patterns: None,
exclude_patterns: None,
limit: Some(limit),
offset: None,
case_sensitive: None,
})
}
fn generate_synthetic_project(root: &Path, module_count: usize) {
let src_dir = root.join("src");
fs::create_dir_all(&src_dir)
.unwrap_or_else(|err| panic!("failed to create src dir {}: {err}", src_dir.display()));
write_file(
src_dir.join("mod0.ts"),
"export function fn0(x: number): number { return x + 1; }\n\
export class Mod0 { ping(): string { return 'mod0'; } }\n",
);
for i in 1..module_count {
let prev = i - 1;
write_file(
src_dir.join(format!("mod{i}.ts")),
&format!(
"import {{ fn{prev}, Mod{prev} }} from './mod{prev}';\n\
export function fn{i}(x: number): number {{ return fn{prev}(x) + 1; }}\n\
export class Mod{i} extends Mod{prev} {{\n\
\x20\x20call{i}(): number {{ return fn{i}({i}); }}\n\
}}\n"
),
);
}
let last = module_count - 1;
write_file(
src_dir.join("index.ts"),
&format!(
"import {{ fn{last}, Mod{last} }} from './mod{last}';\n\
export function entry(): number {{\n\
\x20\x20const m = new Mod{last}();\n\
\x20\x20return fn{last}(0) + m.call{last}();\n\
}}\n"
),
);
}
mod integration_full_pipeline {
use super::*;
#[test]
fn runs_init_index_resolve_search_callers_context_sync() {
let module_count = 120;
let temp_dir = TempProject::new();
generate_synthetic_project(temp_dir.path(), module_count);
let mut guard = CodeGraphGuard::init(temp_dir.path());
let cg = &mut guard.cg;
let index_result = cg.index_all(IndexOptions::default());
assert!(
index_result.files_indexed >= module_count,
"expected at least {module_count} indexed files, got {}",
index_result.files_indexed
);
let stats_after_index = cg.get_stats();
assert!(stats_after_index.file_count >= module_count as u64);
assert!(stats_after_index.node_count > (module_count * 2) as u64);
cg.reinitialize_resolver();
let resolution = cg.resolve_references();
let _total: u64 = resolution.stats.total;
let _resolved: u64 = resolution.stats.resolved;
let entry_results = cg.search_nodes("entry", search_limit(10));
assert!(!entry_results.is_empty());
let entry_node = entry_results
.iter()
.find(|result| result.node.name == "entry");
assert!(entry_node.is_some());
let mid_results = cg.search_nodes("fn50", search_limit(10));
assert!(mid_results.iter().any(|result| result.node.name == "fn50"));
let fn0_results = cg.search_nodes("fn0", search_limit(5));
let fn0_node = fn0_results
.iter()
.find(|result| result.node.name == "fn0")
.expect("fn0 should be indexed");
let callers = cg.get_callers(&fn0_node.node.id, 1);
let _caller_count = callers.len();
let context = cg.build_context(
TaskInput::Query("entry function chain".to_owned()),
Some(BuildContextOptions {
max_nodes: Some(10),
max_code_blocks: None,
max_code_block_size: None,
include_code: None,
format: Some(ContextFormat::Markdown),
search_limit: None,
traversal_depth: None,
min_score: None,
}),
);
match context {
BuildContextResult::Formatted(text) => {
assert!(!text.is_empty(), "markdown context should not be empty");
}
BuildContextResult::Context(_) => panic!("markdown context should be formatted text"),
}
write_file(
temp_dir.path().join("src").join("consumer.ts"),
"import { entry } from './index';\nexport const result = entry();\n",
);
write_file(
temp_dir.path().join("src").join("mod0.ts"),
"export function fn0(x: number): number { return x + 2; }\n\
export function newHelper(): string { return 'new'; }\n\
export class Mod0 { ping(): string { return 'mod0v2'; } }\n",
);
fs::remove_file(temp_dir.path().join("src").join("mod1.ts"))
.expect("failed to remove src/mod1.ts fixture");
let sync_result = cg.sync(IndexOptions::default());
assert!(sync_result.files_added >= 1);
assert!(sync_result.files_modified >= 1);
assert!(sync_result.files_removed >= 1);
assert!(!cg.search_nodes("newHelper", None).is_empty());
let files_after_sync = cg.get_nodes_in_file("src/mod1.ts");
assert!(files_after_sync.is_empty());
}
#[test]
fn keeps_indexing_files_when_one_file_has_a_parse_error() {
let temp_dir = TempProject::new();
let src_dir = temp_dir.path().join("src");
fs::create_dir_all(&src_dir)
.unwrap_or_else(|err| panic!("failed to create src dir {}: {err}", src_dir.display()));
write_file(
src_dir.join("good1.ts"),
"export function good1(): number { return 1; }\n",
);
write_file(
src_dir.join("good2.ts"),
"export function good2(): number { return 2; }\n",
);
write_file(
src_dir.join("broken.ts"),
"export function broken(\n this is { not valid typescript at all\n",
);
let mut guard = CodeGraphGuard::init(temp_dir.path());
let cg = &mut guard.cg;
let result = cg.index_all(IndexOptions::default());
assert!(result.files_indexed >= 2);
let good1 = cg.search_nodes("good1", None);
let good2 = cg.search_nodes("good2", None);
assert!(good1.iter().any(|result| result.node.name == "good1"));
assert!(good2.iter().any(|result| result.node.name == "good2"));
}
#[test]
fn handles_repeated_sync_calls_when_nothing_has_changed() {
let temp_dir = TempProject::new();
generate_synthetic_project(temp_dir.path(), 10);
let mut guard = CodeGraphGuard::init(temp_dir.path());
let cg = &mut guard.cg;
cg.index_all(IndexOptions::default());
let stats_before = cg.get_stats();
let first = cg.sync(IndexOptions::default());
let second = cg.sync(IndexOptions::default());
assert_eq!(
first.files_added + first.files_modified + first.files_removed,
0
);
assert_eq!(
second.files_added + second.files_modified + second.files_removed,
0
);
let stats_after = cg.get_stats();
assert_eq!(stats_after.file_count, stats_before.file_count);
assert_eq!(stats_after.node_count, stats_before.node_count);
}
#[test]
fn reports_edges_created_including_resolution_synthesizer_phases() {
let temp_dir = TempProject::new();
generate_synthetic_project(temp_dir.path(), 30);
let mut guard = CodeGraphGuard::init(temp_dir.path());
let cg = &mut guard.cg;
let result = cg.index_all(IndexOptions::default());
let stats = cg.get_stats();
assert!(result.success);
assert_eq!(result.nodes_created as u64, stats.node_count);
assert_eq!(result.edges_created as u64, stats.edge_count);
let contains_only = *stats.edges_by_kind.get(&EdgeKind::Contains).unwrap_or(&0);
assert!(stats.edge_count > contains_only);
}
}