use magellan::CodeGraph;
use std::path::Path;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn write_and_sync(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::fs::OpenOptions;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
std::io::Write::write_all(&mut file, bytes)?;
file.sync_all()?;
Ok(())
}
#[test]
fn test_default_watch_performs_initial_scan() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path().to_path_buf();
let db_path = temp_dir.path().join("test.db");
let file_path = root_path.join("initial.rs");
write_and_sync(&file_path, b"fn initial() {}").unwrap();
thread::sleep(Duration::from_millis(50));
let mut graph = CodeGraph::open(&db_path).unwrap();
let file_count = graph.scan_directory(&root_path, None).unwrap();
assert_eq!(file_count, 1, "Should have scanned 1 file");
let path_str = file_path.to_string_lossy().to_string();
let symbols = graph.symbols_in_file(&path_str).unwrap();
assert_eq!(symbols.len(), 1, "Should have 1 symbol: initial");
assert_eq!(symbols[0].name.as_deref().unwrap(), "initial");
}
#[test]
fn test_modify_during_scan_is_flushed() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path().to_path_buf();
let db_path = temp_dir.path().join("test.db");
let file_path = root_path.join("modify_test.rs");
let path_str = file_path.to_string_lossy().to_string();
write_and_sync(&file_path, b"fn v0() {}").unwrap();
let file_path_clone = file_path.clone();
let modifier_thread = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
write_and_sync(&file_path_clone, b"fn v1() {}").unwrap();
});
let mut graph = CodeGraph::open(&db_path).unwrap();
let _file_count = graph.scan_directory(&root_path, None).unwrap();
modifier_thread.join().unwrap();
let _ = graph.reconcile_file_path(&file_path, &path_str);
let symbols = graph.symbols_in_file(&path_str).unwrap();
assert_eq!(symbols.len(), 1, "Should have 1 symbol after flush");
assert_eq!(
symbols[0].name.as_deref().unwrap(),
"v1",
"Should have v1 symbol, not v0"
);
}
#[test]
fn test_rapid_modifications_produce_deterministic_final_state() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path().to_path_buf();
let db_path = temp_dir.path().join("test.db");
let file_path = root_path.join("storm.rs");
let path_str = file_path.to_string_lossy().to_string();
write_and_sync(&file_path, b"fn v0() {}").unwrap();
for i in 1..=5 {
write_and_sync(&file_path, format!("fn v{}() {{}}", i).as_bytes()).unwrap();
thread::sleep(Duration::from_millis(10));
}
let mut graph = CodeGraph::open(&db_path).unwrap();
let _ = graph.reconcile_file_path(&file_path, &path_str);
let symbols = graph.symbols_in_file(&path_str).unwrap();
assert_eq!(symbols.len(), 1, "Should have 1 symbol");
assert_eq!(
symbols[0].name.as_deref().unwrap(),
"v5",
"Should have v5 (last write)"
);
}
#[test]
fn test_watch_only_skips_baseline_scan() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path().to_path_buf();
let db_path = temp_dir.path().join("test.db");
let before_file = root_path.join("before.rs");
write_and_sync(&before_file, b"fn before() {}").unwrap();
thread::sleep(Duration::from_millis(50));
let mut graph = CodeGraph::open(&db_path).unwrap();
let file_count = graph.count_files().unwrap();
assert_eq!(file_count, 0, "DB should be empty before scan");
assert!(before_file.exists(), "File should exist on disk");
let path_str = before_file.to_string_lossy().to_string();
let symbols = graph.symbols_in_file(&path_str).unwrap();
assert_eq!(symbols.len(), 0, "File should not be indexed without scan");
let _ = graph.reconcile_file_path(&before_file, &path_str);
let symbols = graph.symbols_in_file(&path_str).unwrap();
assert_eq!(symbols.len(), 1, "File should be indexed after reconcile");
assert_eq!(symbols[0].name.as_deref().unwrap(), "before");
}
#[test]
fn test_deterministic_batch_ordering() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path().to_path_buf();
let db_path = temp_dir.path().join("test.db");
let files = vec!["zebra.rs", "alpha.rs", "beta.rs"];
for file_name in &files {
let file_path = root_path.join(file_name);
write_and_sync(
&file_path,
format!("fn {}() {{}}", file_name.replace(".rs", "")).as_bytes(),
)
.unwrap();
}
let mut graph = CodeGraph::open(&db_path).unwrap();
graph.scan_directory(&root_path, None).unwrap();
let all_files = graph.all_file_nodes().unwrap();
assert_eq!(all_files.len(), 3, "Should have 3 files");
let mut file_names: Vec<_> = all_files.keys().collect();
file_names.sort();
assert_eq!(
file_names[0].as_str(),
root_path.join("alpha.rs").to_string_lossy().as_ref()
);
assert_eq!(
file_names[1].as_str(),
root_path.join("beta.rs").to_string_lossy().as_ref()
);
assert_eq!(
file_names[2].as_str(),
root_path.join("zebra.rs").to_string_lossy().as_ref()
);
}
#[test]
fn test_multiple_files_modified_during_scan() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path().to_path_buf();
let db_path = temp_dir.path().join("test.db");
let file1 = root_path.join("file1.rs");
let file2 = root_path.join("file2.rs");
write_and_sync(&file1, b"fn v1_0() {}").unwrap();
write_and_sync(&file2, b"fn v2_0() {}").unwrap();
thread::sleep(Duration::from_millis(50));
let file1_clone = file1.clone();
let file2_clone = file2.clone();
let modifier_thread = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
write_and_sync(&file1_clone, b"fn v1_1() {}").unwrap();
write_and_sync(&file2_clone, b"fn v2_1() {}").unwrap();
});
let mut graph = CodeGraph::open(&db_path).unwrap();
graph.scan_directory(&root_path, None).unwrap();
modifier_thread.join().unwrap();
let path1_str = file1.to_string_lossy().to_string();
let path2_str = file2.to_string_lossy().to_string();
let _ = graph.reconcile_file_path(&file1, &path1_str);
let _ = graph.reconcile_file_path(&file2, &path2_str);
let symbols1 = graph.symbols_in_file(&path1_str).unwrap();
assert_eq!(symbols1.len(), 1);
assert_eq!(symbols1[0].name.as_deref().unwrap(), "v1_1");
let symbols2 = graph.symbols_in_file(&path2_str).unwrap();
assert_eq!(symbols2.len(), 1);
assert_eq!(symbols2[0].name.as_deref().unwrap(), "v2_1");
}