use std::fs;
use std::path::Path;
use codenexus::index::IndexFacade;
use codenexus::model::NodeLabel;
use codenexus::query::QueryFacade;
use tempfile::TempDir;
use crossbeam_channel::unbounded;
use inklog::domain::core::LoggerSubscriber;
use inklog::{LogRecord, Metrics};
use std::cell::RefCell;
use std::sync::Arc;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
fn write_file(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
fn fresh_db_path() -> std::path::PathBuf {
let dir = TempDir::new().unwrap();
let path = dir.path().join("integration_testdb");
std::mem::forget(dir);
path
}
fn build_multilang_repo(dir: &Path) {
write_file(
dir,
"src/main.rs",
"\
fn main() {
helper();
}
fn helper() {
let x = 42;
println!(\"{}\", x);
}
extern \"C\" {
fn c_bridge(input: i32) -> i32;
}
",
);
write_file(
dir,
"src/c_bridge.c",
"\
#include <stdio.h>
int c_bridge(int input) {
return input * 2;
}
",
);
write_file(
dir,
"src/c_bridge.h",
"\
#ifndef C_BRIDGE_H
#define C_BRIDGE_H
int c_bridge(int input);
#endif
",
);
}
#[test]
fn index_multilang_repo_succeeds() {
let tmp = TempDir::new().unwrap();
build_multilang_repo(tmp.path());
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result = facade.index(tmp.path(), "demo", false).expect("index");
assert!(!result.project_id.is_empty(), "project_id 应非空");
assert!(
result.files_indexed >= 2,
"至少索引 2 个文件,got {}",
result.files_indexed
);
assert!(
result.nodes_created > 0,
"应创建节点,got {}",
result.nodes_created
);
}
#[test]
fn index_creates_project_node() {
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "main.rs", "fn main() {}\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade
.index(tmp.path(), "my_project", false)
.expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let result = query
.cypher("MATCH (p:Project) RETURN p.name AS name LIMIT 10;")
.expect("cypher");
let names: Vec<String> = result
.rows
.into_iter()
.filter_map(|row| {
row.into_iter()
.next()
.and_then(|v| v.as_str().map(String::from))
})
.collect();
assert!(
names.iter().any(|n| n == "my_project"),
"应存在名为 my_project 的 Project 节点,got {names:?}"
);
}
#[test]
fn multi_project_isolation() {
let tmp1 = TempDir::new().unwrap();
let tmp2 = TempDir::new().unwrap();
write_file(tmp1.path(), "a.rs", "fn alpha() {}\n");
write_file(tmp2.path(), "b.rs", "fn beta() {}\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result_a = facade
.index(tmp1.path(), "project_a", false)
.expect("index a");
let result_b = facade
.index(tmp2.path(), "project_b", false)
.expect("index b");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let results_a = query
.search("alpha", Some(&result_a.project_id), 10)
.expect("search a");
assert!(
results_a.iter().any(|r| r.name.contains("alpha")),
"project_a 应含 alpha"
);
let results_b = query
.search("beta", Some(&result_b.project_id), 10)
.expect("search b");
assert!(
results_b.iter().any(|r| r.name.contains("beta")),
"project_b 应含 beta"
);
let cross = query
.search("beta", Some(&result_a.project_id), 10)
.expect("search cross");
assert!(
!cross.iter().any(|r| r.name.contains("beta")),
"project_a 不应含 beta(多项目隔离)"
);
}
#[test]
fn gitignore_target_dir_skipped() {
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "main.rs", "fn main() {}\n");
write_file(tmp.path(), ".gitignore", "target/\n");
write_file(tmp.path(), "target/build.rs", "fn should_skip() {}\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result = facade.index(tmp.path(), "demo", false).expect("index");
assert_eq!(
result.files_indexed, 1,
"target/ 应被 .gitignore 跳过,实际索引 {} 个文件",
result.files_indexed
);
}
#[test]
fn cypher_query_after_index() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"lib.rs",
"pub fn parse_input(s: &str) -> Vec<u8> { s.as_bytes().to_vec() }\n",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "demo", false).expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let result = query
.cypher("MATCH (f:Function) RETURN f.name AS name LIMIT 5;")
.expect("cypher");
assert!(!result.columns.is_empty(), "应返回列");
assert!(!result.rows.is_empty(), "应返回至少一行");
}
#[test]
fn structured_search_by_name() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"main.rs",
"fn parse_config() {}\nfn read_file() {}\n",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "demo", false).expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let results = query.search("parse", None, 10).expect("search");
assert!(
results.iter().any(|r| r.name.contains("parse")),
"结构化搜索应找到 parse_config"
);
}
#[test]
fn structured_search_by_type() {
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "main.rs", "fn main() {}\nstruct Config;\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "demo", false).expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let results = query
.search_by_type(NodeLabel::Struct, None, 10)
.expect("search_by_type");
assert!(
results.iter().any(|r| r.name.contains("Config")),
"按类型搜索应找到 Struct 节点 Config"
);
}
#[test]
fn fulltext_search_finds_matches() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"lib.rs",
"pub fn parse_json(input: &str) -> Value {}\npub fn parse_xml(input: &str) -> Value {}\n",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "demo", false).expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let results = query.fulltext_search("parse", None, 10).expect("fulltext");
assert!(!results.is_empty(), "全文搜索 parse 应返回结果");
}
#[test]
fn incremental_index_skips_unchanged_files() {
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "a.rs", "fn a() {}\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result1 = facade.index(tmp.path(), "demo", false).expect("index 1");
assert_eq!(result1.files_indexed, 1, "首次应索引 1 个文件");
let result2 = facade.index(tmp.path(), "demo", false).expect("index 2");
assert_eq!(result2.files_skipped, 1, "第二次应跳过未变更文件");
}
#[test]
fn incremental_index_detects_new_file() {
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "a.rs", "fn a() {}\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "demo", false).expect("index 1");
write_file(tmp.path(), "b.rs", "fn b() {}\n");
let result2 = facade.index(tmp.path(), "demo", false).expect("index 2");
assert!(
result2.files_indexed >= 1,
"应索引新增的 b.rs,got {}",
result2.files_indexed
);
}
#[test]
fn force_reindexes_all_files() {
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "a.rs", "fn a() {}\n");
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "demo", false).expect("index 1");
let result2 = facade.index(tmp.path(), "demo", true).expect("index force");
assert_eq!(result2.files_indexed, 1, "--force 应重解析所有文件");
}
#[test]
fn index_nonexistent_path_returns_error() {
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result = facade.index(Path::new("/nonexistent/path"), "demo", false);
assert!(result.is_err(), "不存在的路径应返回错误");
}
#[test]
fn index_python_file() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"main.py",
"\
def greet(name):
return f\"Hello, {name}!\"
class Greeter:
def __init__(self):
self.name = \"world\"
",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result = facade.index(tmp.path(), "py_demo", false).expect("index");
assert!(result.files_indexed >= 1, "应索引 Python 文件");
assert!(result.nodes_created > 0, "应创建节点");
}
#[test]
fn index_typescript_file() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"main.ts",
"\
function add(a: number, b: number): number {
return a + b;
}
class Calculator {
add(a: number, b: number): number {
return add(a, b);
}
}
",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result = facade.index(tmp.path(), "ts_demo", false).expect("index");
assert!(result.files_indexed >= 1, "应索引 TypeScript 文件");
}
#[test]
fn index_fortran_file() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"main.f90",
"\
module math_utils
implicit none
contains
function square(x) result(y)
integer, intent(in) :: x
integer :: y
y = x * x
end function square
end module math_utils
",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
let result = facade.index(tmp.path(), "f90_demo", false).expect("index");
assert!(result.files_indexed >= 1, "应索引 Fortran 文件");
}
thread_local! {
static TRACING_GUARD: RefCell<Option<tracing::subscriber::DefaultGuard>> =
const { RefCell::new(None) };
}
fn format_record(record: &LogRecord) -> String {
let mut output = record.message.clone();
for (key, value) in &record.fields {
output.push(' ');
output.push_str(key);
output.push('=');
output.push_str(&value.to_string());
}
output
}
fn drain_to_string(rx: &crossbeam_channel::Receiver<Arc<LogRecord>>) -> String {
let mut output = String::new();
while let Ok(record) = rx.try_recv() {
output.push_str(&format_record(&record));
output.push('\n');
}
output
}
#[test]
fn index_emits_all_log_events() {
use rayon::ThreadPoolBuilder;
let (console_tx, console_rx) = unbounded::<Arc<LogRecord>>();
let (async_tx, _async_rx) = unbounded::<Arc<LogRecord>>();
let metrics = Arc::new(Metrics::new());
let main_layer = LoggerSubscriber::new(console_tx.clone(), async_tx.clone(), metrics.clone())
.with_filter(LevelFilter::DEBUG);
let main_registry = tracing_subscriber::registry().with(main_layer);
let console_tx_for_handler = console_tx.clone();
let async_tx_for_handler = async_tx.clone();
let metrics_for_handler = metrics.clone();
let pool = ThreadPoolBuilder::new()
.start_handler(move |_idx| {
let worker_layer = LoggerSubscriber::new(
console_tx_for_handler.clone(),
async_tx_for_handler.clone(),
metrics_for_handler.clone(),
)
.with_filter(LevelFilter::DEBUG);
let worker_registry = tracing_subscriber::registry().with(worker_layer);
let guard = tracing::subscriber::set_default(worker_registry);
TRACING_GUARD.with(|g| *g.borrow_mut() = Some(guard));
})
.build()
.expect("rayon thread pool");
let tmp = TempDir::new().unwrap();
write_file(tmp.path(), "a.rs", "fn a() {}\n");
write_file(tmp.path(), "b.rs", "fn b() {}\n");
let db = fresh_db_path();
let src_path = tmp.path().to_path_buf();
tracing::subscriber::with_default(main_registry, || {
pool.install(|| {
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(&src_path, "log_e2e", false).expect("index");
});
});
let captured = drain_to_string(&console_rx);
assert!(
captured.contains("index_started"),
"LOG-001: index_started event missing, got: {captured:?}"
);
assert!(
captured.contains("index_completed"),
"LOG-001: index_completed event missing, got: {captured:?}"
);
assert!(
captured.contains("file_parsed"),
"LOG-002: file_parsed event missing, got: {captured:?}"
);
let file_parsed_count = captured.matches("file_parsed").count();
assert!(
file_parsed_count >= 2,
"LOG-002: expected at least 2 file_parsed events (one per file), got {file_parsed_count}"
);
assert!(
captured.contains("performance"),
"LOG-006: performance event missing, got: {captured:?}"
);
assert!(
captured.contains("files_per_second"),
"LOG-006: performance event should carry files_per_second field, got: {captured:?}"
);
}
#[test]
fn reads_writes_edges_exist_after_multilang_index() {
let tmp = TempDir::new().unwrap();
write_file(
tmp.path(),
"main.rs",
"fn caller(x: i32) -> i32 {\n let y = x + 1;\n y\n}\n",
);
write_file(
tmp.path(),
"main.c",
"int caller(int x) {\n int y = x + 1;\n return y;\n}\n",
);
write_file(
tmp.path(),
"main.py",
"def caller(x):\n y = x + 1\n return y\n",
);
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "rw_e2e", false).expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let result = query
.cypher("MATCH (r:CodeRelation) RETURN r.type AS type;")
.expect("cypher CodeRelation");
let reads_count = result
.rows
.iter()
.filter(|row| row.first().and_then(|v| v.as_str()) == Some("READS"))
.count();
let writes_count = result
.rows
.iter()
.filter(|row| row.first().and_then(|v| v.as_str()) == Some("WRITES"))
.count();
assert!(
reads_count > 0,
"graph should contain at least one READS edge (BR-TRACE-005), got {reads_count}"
);
assert!(
writes_count > 0,
"graph should contain at least one WRITES edge (BR-TRACE-006), got {writes_count}"
);
}
#[test]
fn ffi_edge_exists_after_multilang_index() {
let tmp = TempDir::new().unwrap();
build_multilang_repo(tmp.path());
let db = fresh_db_path();
let facade = IndexFacade::new(&db).expect("IndexFacade::new");
facade.index(tmp.path(), "multilang", false).expect("index");
let query = QueryFacade::new(&db).expect("QueryFacade::new");
let result = query
.cypher("MATCH (r:CodeRelation) RETURN r.type AS type;")
.expect("cypher CodeRelation");
let ffi_count = result
.rows
.iter()
.filter(|row| row.first().and_then(|v| v.as_str()) == Some("FFI_CALLS"))
.count();
assert!(
ffi_count >= 1,
"FFI 索引后应至少有 1 条 FfiCalls 边,实际 {}",
ffi_count
);
}
#[tokio::test]
async fn ffi_trace_returns_cross_language_path() {
use codenexus::kit::{build_kit, IndexerModule, KitBootstrapConfig, TraceModule};
use codenexus::model::EdgeType;
let tmp = TempDir::new().unwrap();
build_multilang_repo(tmp.path());
let db = fresh_db_path();
let kit = build_kit(&KitBootstrapConfig::new(db.clone()))
.await
.expect("build_kit");
let indexer = kit.require::<IndexerModule>().expect("require_indexer");
indexer
.index(tmp.path(), "multilang", false)
.expect("index");
let trace = kit.require::<TraceModule>().expect("require_trace");
let graph = trace.load_graph("c_bridge", 3).expect("load_graph");
assert!(
graph
.edges
.iter()
.any(|e| e.edge_type == EdgeType::FfiCalls),
"trace 应返回含 FfiCalls 边的跨语言路径,got edges: {:?}",
graph.edges.iter().map(|e| e.edge_type).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn corrupt_db_returns_exit_code_4() {
use codenexus::index::IndexError;
use codenexus::kit::{build_kit, KitBootstrapConfig, KitError};
use codenexus::storage::StorageError;
let dir = TempDir::new().unwrap();
let lbug_file = dir.path().join("corrupt.lbug");
std::fs::write(&lbug_file, b"this is not a valid ladybugdb file").expect("write corrupt file");
std::mem::forget(dir);
let config = KitBootstrapConfig::new(lbug_file);
let result = build_kit(&config).await;
let kit_err = result.expect_err("build_kit 应在损坏数据库上失败");
let build_failed_source = match &kit_err {
KitError::BuildFailed { source, .. } => source.as_ref(),
other => panic!("期望 KitError::BuildFailed,实际 {other:?}"),
};
let storage_err = build_failed_source
.downcast_ref::<StorageError>()
.unwrap_or_else(|| {
panic!(
"期望 BuildFailed.source 为 StorageError,实际: {:?}",
build_failed_source
);
});
assert!(
matches!(storage_err, StorageError::Corrupt(_)),
"期望 StorageError::Corrupt,实际: {storage_err:?}"
);
let index_err: IndexError = StorageError::Corrupt("test corrupt".to_string()).into();
assert!(
matches!(index_err, IndexError::DatabaseCorrupt(_)),
"期望 IndexError::DatabaseCorrupt,实际: {index_err:?}"
);
assert_eq!(
index_err.exit_code(),
4,
"IndexError::DatabaseCorrupt exit_code 必须为 4 (PRD §4.1.6)"
);
}