use super::*;
use std::fs;
impl WorkspaceIndex {
fn apply_test_changes(&self, changes: Vec<Change>) {
apply_changes(&self.shared, changes);
}
fn cancel_only(&self) {
self.shared.cancel.store(true, Ordering::Relaxed);
}
fn send_batch(&self, changes: Vec<Change>) {
let _ = self.shared.cmd_tx.send(Command::Batch(changes));
}
}
fn fixture() -> tempfile::TempDir {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("src/nested")).unwrap();
fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(root.join("src/nested/deep.rs"), "// deep").unwrap();
fs::write(root.join("Cargo.toml"), "[package]").unwrap();
fs::create_dir_all(root.join("target/debug")).unwrap();
fs::write(root.join("target/debug/x.o"), "bin").unwrap();
fs::write(root.join(".env"), "SECRET=1").unwrap();
dir
}
#[test]
fn end_to_end_scan_query_browse() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
match idx.status() {
ScanStatus::Ready { total, truncated } => {
assert!(total >= 5, "total={total}");
assert!(!truncated);
}
other => panic!("expected Ready, got {other:?}"),
}
let hits = idx.query("main.rs", 10);
assert!(!hits.is_empty());
assert_eq!(hits[0].rel, "src/main.rs");
assert_eq!(hits[0].root, 0);
assert!(idx.query("x.o", 10).is_empty());
assert!(idx.query(".env", 10).is_empty());
let top = idx.query("", 20);
assert!(top.iter().any(|m| m.rel == "src" && m.is_dir));
assert!(top.iter().any(|m| m.rel == "Cargo.toml" && !m.is_dir));
assert!(top[0].is_dir, "dirs first: {top:?}");
let src = idx.query("src/", 20);
assert!(src.iter().any(|m| m.rel == "src/main.rs"));
assert!(src.iter().all(|m| m.rel.starts_with("src/")));
assert!(idx.entries().iter().any(|e| &*e.rel == "src/main.rs"));
let mut seen = 0;
idx.visit(&mut |_| {
seen += 1;
true
});
assert!(seen >= 5);
let mut stopped = 0;
idx.visit(&mut |_| {
stopped += 1;
false
});
assert_eq!(stopped, 1);
let m = &hits[0];
assert!(idx.resolve(m).ends_with("src/main.rs"));
}
#[test]
fn watcher_picks_up_changes() {
let dir = fixture();
let idx = WorkspaceIndex::start(vec![dir.path().to_path_buf()]);
assert!(idx.wait_ready(Duration::from_secs(10)));
let before = idx.len();
fs::write(dir.path().join("src/new_file.rs"), "// new").unwrap();
let deadline = Instant::now() + Duration::from_secs(15);
let appeared = loop {
let hit = idx
.query("new_file", 10)
.iter()
.any(|m| m.rel == "src/new_file.rs");
if hit || Instant::now() >= deadline {
break hit;
}
std::thread::sleep(Duration::from_millis(50));
};
assert!(appeared, "create must be indexed");
assert_eq!(idx.len(), before + 1, "create adds one store entry");
fs::remove_file(dir.path().join("src/new_file.rs")).unwrap();
let deadline = Instant::now() + Duration::from_secs(15);
loop {
let gone = {
let mut found = false;
idx.visit(&mut |e| {
found |= &*e.rel == "src/new_file.rs";
true
});
!found
};
if gone || Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
let mut found = false;
idx.visit(&mut |e| {
found |= &*e.rel == "src/new_file.rs";
true
});
assert!(!found, "delete must be removed from store");
}
#[test]
fn sanitize_roots_dedups_nested() {
let dir = fixture();
let root = dir.path().canonicalize().unwrap();
let roots = sanitize_roots(vec![
root.clone(),
root.join("src"), root.join("missing"), root.clone(), ]);
assert_eq!(roots, vec![root]);
}
#[test]
fn project_roots_without_allowlist() {
let dir = fixture();
let roots = WorkspaceIndex::project_roots(dir.path());
assert_eq!(roots, vec![dir.path().to_path_buf()]);
}
#[test]
fn project_roots_reads_allowlist() {
let dir = fixture();
let ext = tempfile::TempDir::new().unwrap();
fs::create_dir_all(dir.path().join(".whycodes")).unwrap();
fs::write(
dir.path().join(".whycodes/external_dirs_allowed"),
format!("# comment\n{}\n\n", ext.path().display()),
)
.unwrap();
let roots = WorkspaceIndex::project_roots(dir.path());
assert_eq!(roots.len(), 2);
assert_eq!(roots[0], dir.path());
assert_eq!(roots[1], ext.path());
}
#[test]
fn empty_roots_is_safe() {
let idx = WorkspaceIndex::start_with(vec![], IndexOptions::default());
assert!(idx.roots().is_empty());
assert!(idx.primary_root().is_none());
assert!(idx.query("x", 5).is_empty());
}
#[test]
fn async_query_eventually_consistent() {
let dir = tempfile::TempDir::new().unwrap();
for d in 0..50 {
let p = dir.path().join(format!("pkg{d}/src"));
fs::create_dir_all(&p).unwrap();
for f in 0..100 {
fs::write(p.join(format!("file{f}.rs")), "x").unwrap();
}
}
fs::write(dir.path().join("pkg7/src/needle.rs"), "x").unwrap();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(30)));
let t = Instant::now();
let _ = idx.query_now("needle.rs", 10);
let first_ms = t.elapsed().as_secs_f64() * 1000.0;
assert!(first_ms < 50.0, "query_now blocked {first_ms:.1}ms");
let deadline = Instant::now() + Duration::from_secs(5);
let mut found = false;
while Instant::now() < deadline {
let dirty = idx.take_results_dirty();
let running = idx.matching();
if dirty || !running {
let hits = idx.read_matches(10);
if hits.iter().any(|m| m.rel.ends_with("needle.rs")) {
found = true;
break;
}
}
std::thread::sleep(Duration::from_millis(2));
}
assert!(found, "needle.rs never appeared via dirty-flag polling");
let deadline = Instant::now() + Duration::from_secs(5);
while idx.matching() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(2));
}
assert!(!idx.matching());
}
#[test]
fn cancel_aborts_scan_and_loop() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: true,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
std::thread::sleep(Duration::from_millis(300));
idx.cancel_only();
std::thread::sleep(Duration::from_millis(300));
let _ = idx.query_now("rs", 20);
let _ = idx.matching();
let hits = idx.read_matches(20);
let _ = hits.len();
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
idx.cancel_only();
let _ = idx.wait_ready(Duration::from_millis(200));
}
#[test]
fn fill_root_stops_reading_files_once_cancelled() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
let entry = |rel: &str| WalkEntry {
rel: rel.into(),
is_dir: false,
size: 0,
};
let content = || read(&idx.shared.states[0].content);
write(&idx.shared.states[0].content).clear();
fill_root(&idx.shared, 0, vec![entry("src/main.rs")]);
assert_eq!(content().indexed_len(), 1);
assert!(content().is_complete());
write(&idx.shared.states[0].content).clear();
idx.cancel_only();
fill_root(&idx.shared, 0, vec![entry("src/main.rs")]);
assert_eq!(content().indexed_len(), 0);
assert!(!content().is_complete());
}
#[test]
fn fill_root_reads_files_without_holding_the_store_lock() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
let entry = WalkEntry {
rel: "src/main.rs".into(),
is_dir: false,
size: 0,
};
let content = write(&idx.shared.states[0].content);
let filled = std::thread::scope(|s| {
let filler = s.spawn(|| fill_root(&idx.shared, 0, vec![entry]));
let deadline = Instant::now() + Duration::from_secs(5);
let mut filled = false;
while !filled && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(2));
filled = idx.shared.states[0].store.try_read().is_ok_and(|store| {
store
.entries()
.iter()
.any(|e| &*e.rel == "src/main.rs" && e.size == 0)
});
}
drop(content);
filler.join().unwrap();
filled
});
assert!(filled, "store fill waited on the content lock");
}
#[test]
fn log_unwatch_spawn_ok_and_err() {
log_unwatch_spawn(std::thread::Builder::new().spawn(|| {}));
log_unwatch_spawn(Err(std::io::Error::other("no thread")));
}
#[test]
fn query_now_same_pattern_after_blocking_still_hits() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
assert!(
idx.query("main", 10).iter().any(|m| m.rel == "src/main.rs"),
"blocking fuzzy missed src/main.rs"
);
assert!(
idx.query_now("main", 10)
.iter()
.any(|m| m.rel == "src/main.rs"),
"same-pattern query_now missed src/main.rs"
);
assert!(idx.query_now("zzzznotapath", 10).is_empty());
}
#[test]
fn query_now_sorts_multiple_hits() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
let _ = idx.query_now("rs", 20);
idx.rearm_fuzzy();
let deadline = Instant::now() + Duration::from_secs(5);
let mut hits = Vec::new();
while Instant::now() < deadline {
let _ = idx.matching();
hits = idx.read_matches(20);
if hits.len() >= 2 {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(hits.len() >= 2, "{hits:?}");
}
#[test]
fn blocking_query_merges_and_sorts_across_roots() {
let a = fixture();
let b = fixture();
let idx = WorkspaceIndex::start_with(
vec![a.path().to_path_buf(), b.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
let hits = idx.query("rs", 20);
assert!(hits.len() >= 4, "{hits:?}");
for w in hits.windows(2) {
assert!(
w[0].score > w[1].score
|| (w[0].score == w[1].score && w[0].rel.len() <= w[1].rel.len()),
"unsorted: {w:?}"
);
}
let tied = idx.query("main.rs", 10);
assert_eq!(tied.len(), 2, "{tied:?}");
assert_eq!(tied[0].score, tied[1].score);
assert!(tied.iter().any(|m| m.root == 0));
assert!(tied.iter().any(|m| m.root == 1));
}
#[test]
fn recv_should_stop_only_on_disconnect() {
assert!(!recv_should_stop(
std::sync::mpsc::RecvTimeoutError::Timeout
));
assert!(recv_should_stop(
std::sync::mpsc::RecvTimeoutError::Disconnected
));
assert!(matches!(
classify_recv(Err(std::sync::mpsc::RecvTimeoutError::Timeout)),
RecvAct::Idle
));
assert!(matches!(
classify_recv(Err(std::sync::mpsc::RecvTimeoutError::Disconnected)),
RecvAct::Stop
));
assert!(matches!(
classify_recv(Ok(Command::Shutdown)),
RecvAct::Stop
));
assert!(matches!(
classify_recv(Ok(Command::Rescan)),
RecvAct::Rescan
));
assert!(matches!(
classify_recv(Ok(Command::Batch(vec![]))),
RecvAct::Batch(_)
));
}
#[test]
fn start_helpers_status_browse_and_rescan() {
let dir = fixture();
let file = dir.path().join("not-a-dir.txt");
fs::write(&file, "x").unwrap();
let idx = WorkspaceIndex::start(vec![
dir.path().to_path_buf(),
file,
PathBuf::from("/no/such/index/root"),
]);
assert!(idx.wait_ready(Duration::from_secs(10)));
assert_eq!(
idx.primary_root(),
Some(dir.path().canonicalize().unwrap().as_path())
);
assert!(!idx.is_empty());
let _ = format!("{:?}", idx);
match idx.status() {
ScanStatus::Ready { .. } => {}
other => panic!("{other:?}"),
}
assert!(idx.browse(99, "").is_empty());
let now = idx.query_now("", 10);
assert!(now.iter().any(|m| m.rel == "src" && m.is_dir));
let src = idx.query_now("src/", 10);
assert!(src.iter().any(|m| m.rel.starts_with("src/")));
idx.rescan();
let deadline = Instant::now() + Duration::from_secs(10);
while idx.is_ready() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(2));
}
assert!(idx.wait_ready(Duration::from_secs(10)));
assert!(idx.entries().iter().any(|e| &*e.rel == "src/main.rs"));
let _ = idx.query_now("rs", 20);
let _ = idx.read_matches(20);
}
#[test]
fn scanning_status_before_ready_and_empty_visit() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
max_entries: DEFAULT_MAX_ENTRIES,
},
);
let _ = idx.status();
let _ = idx.is_empty();
assert!(idx.wait_ready(Duration::from_secs(10)));
let empty = WorkspaceIndex::start_with(vec![], IndexOptions::default());
assert!(empty.is_empty());
empty.visit(&mut |_| panic!("no entries"));
assert!(empty.entries().is_empty());
assert!(empty.query_now("x", 5).is_empty());
assert!(empty.query("x", 5).is_empty());
}
#[test]
fn max_entries_one_truncates_walk() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
max_entries: 1,
threads: 1,
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
match idx.status() {
ScanStatus::Ready { truncated, .. } => assert!(truncated),
other => panic!("{other:?}"),
}
}
#[test]
fn max_entries_zero_truncates() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
max_entries: 0,
threads: 1,
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
match idx.status() {
ScanStatus::Ready { truncated, .. } => assert!(truncated),
other => panic!("{other:?}"),
}
}
#[test]
fn apply_changes_upsert_and_remove() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
let new = dir.path().join("src/extra.rs");
fs::write(&new, "fn extra() {}").unwrap();
idx.apply_test_changes(vec![Change {
root: 0,
rel: "src/extra.rs".into(),
kind: ChangeKind::Upsert,
}]);
let mut found = false;
idx.visit(&mut |e| {
found |= &*e.rel == "src/extra.rs";
true
});
assert!(found, "upsert must land in the store");
fs::remove_file(&new).unwrap();
idx.apply_test_changes(vec![Change {
root: 0,
rel: "src/extra.rs".into(),
kind: ChangeKind::Upsert,
}]);
found = false;
idx.visit(&mut |e| {
found |= &*e.rel == "src/extra.rs";
true
});
assert!(!found, "gone file collapses to remove");
idx.apply_test_changes(vec![Change {
root: 0,
rel: "src/main.rs".into(),
kind: ChangeKind::Remove,
}]);
found = false;
idx.visit(&mut |e| {
found |= &*e.rel == "src/main.rs";
true
});
assert!(!found);
idx.apply_test_changes(vec![Change {
root: 9,
rel: "nope.rs".into(),
kind: ChangeKind::Remove,
}]);
}
#[test]
fn scanner_applies_batched_command() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
fs::write(dir.path().join("src/batch.rs"), "// batch").unwrap();
idx.send_batch(vec![Change {
root: 0,
rel: "src/batch.rs".into(),
kind: ChangeKind::Upsert,
}]);
let deadline = Instant::now() + Duration::from_secs(5);
let appeared = loop {
let mut found = false;
idx.visit(&mut |e| {
found |= &*e.rel == "src/batch.rs";
true
});
if found || Instant::now() >= deadline {
break found;
}
std::thread::sleep(Duration::from_millis(20));
};
assert!(appeared, "scanner_main must apply Command::Batch");
}
#[test]
fn lock_helpers_survive_poison() {
fn poison<T: Send + 'static>(make: fn() -> T) {
let m = std::sync::Arc::new(std::sync::Mutex::new(make()));
let m2 = std::sync::Arc::clone(&m);
let _ = std::thread::spawn(move || {
let _g = m2.lock().unwrap();
panic!("poison");
})
.join();
let _g = lock(&m);
}
poison(|| 1u8);
let r = std::sync::Arc::new(std::sync::RwLock::new(0u8));
let r2 = std::sync::Arc::clone(&r);
let _ = std::thread::spawn(move || {
let _g = r2.write().unwrap();
panic!("poison");
})
.join();
drop(read(&r));
let w = std::sync::Arc::new(std::sync::RwLock::new(0u8));
let w2 = std::sync::Arc::clone(&w);
let _ = std::thread::spawn(move || {
let _g = w2.write().unwrap();
panic!("poison");
})
.join();
drop(write(&w));
poison(FuzzyEngine::default);
poison(Vec::<WalkEntry>::new);
let store = std::sync::Arc::new(std::sync::RwLock::new(IndexStore::new()));
let store2 = std::sync::Arc::clone(&store);
let _ = std::thread::spawn(move || {
let _g = store2.write().unwrap();
panic!("poison");
})
.join();
drop(read(&store));
let store = std::sync::Arc::new(std::sync::RwLock::new(IndexStore::new()));
let store2 = std::sync::Arc::clone(&store);
let _ = std::thread::spawn(move || {
let _g = store2.write().unwrap();
panic!("poison");
})
.join();
drop(write(&store));
}
#[test]
fn visit_content_candidates_prunes_and_scopes() {
let dir = fixture();
fs::write(dir.path().join("src/main.rs"), "fn unique_needle() {}\n").unwrap();
fs::write(dir.path().join("src/nested/deep.rs"), "// other\n").unwrap();
fs::write(dir.path().join("srcx.rs"), "fn unique_needle() {}\n").unwrap();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
let mut hits = Vec::new();
assert!(
idx.visit_content_candidates(dir.path(), "unique_needle", false, &mut |_, rel, _| {
hits.push(rel.to_string());
true
})
.is_some()
);
assert!(hits.contains(&"src/main.rs".to_string()), "{hits:?}");
assert!(hits.contains(&"srcx.rs".to_string()), "{hits:?}");
assert_eq!(hits.len(), 2, "{hits:?}");
let mut scoped = Vec::new();
assert!(
idx.visit_content_candidates(
&dir.path().join("src"),
"unique_needle",
false,
&mut |_, rel, _| {
scoped.push(rel.to_string());
true
}
)
.is_some()
);
assert_eq!(scoped, vec!["main.rs".to_string()]);
assert!(!scoped.iter().any(|r| r.contains("srcx")), "{scoped:?}");
let mut stopped = 0;
idx.visit_content_candidates(dir.path(), "unique_needle", false, &mut |_, _, _| {
stopped += 1;
false
});
assert_eq!(stopped, 1);
assert!(
idx.visit_content_candidates(dir.path(), "unique.needle", false, &mut |_, _, _| true)
.is_none(),
"regex must not prune"
);
assert!(
idx.visit_content_candidates(dir.path(), "ab", false, &mut |_, _, _| true)
.is_none()
);
assert!(
idx.visit_content_candidates(
Path::new("/no/such/grep/root"),
"unique_needle",
false,
&mut |_, _, _| true
)
.is_none()
);
let empty = WorkspaceIndex::start_with(vec![], IndexOptions::default());
assert!(
empty
.visit_content_candidates(Path::new("."), "unique_needle", false, &mut |_, _, _| true)
.is_none()
);
}
#[test]
fn visit_content_candidates_incomplete_when_truncated_empty() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
max_entries: 0,
threads: 1,
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
assert!(
idx.visit_content_candidates(dir.path(), "unique_needle", false, &mut |_, _, _| true)
.is_none()
);
}
#[test]
fn apply_changes_updates_content_overlay() {
let dir = fixture();
let idx = WorkspaceIndex::start_with(
vec![dir.path().to_path_buf()],
IndexOptions {
watch: false,
threads: 1,
..Default::default()
},
);
assert!(idx.wait_ready(Duration::from_secs(10)));
fs::write(dir.path().join("src/extra.rs"), "fn extra_unique() {}\n").unwrap();
idx.apply_test_changes(vec![Change {
root: 0,
rel: "src/extra.rs".into(),
kind: ChangeKind::Upsert,
}]);
let mut hits = Vec::new();
idx.visit_content_candidates(dir.path(), "extra_unique", false, &mut |_, rel, _| {
hits.push(rel.to_string());
true
});
assert_eq!(hits, vec!["src/extra.rs".to_string()]);
fs::remove_file(dir.path().join("src/extra.rs")).unwrap();
idx.apply_test_changes(vec![Change {
root: 0,
rel: "src/extra.rs".into(),
kind: ChangeKind::Remove,
}]);
hits.clear();
idx.visit_content_candidates(dir.path(), "extra_unique", false, &mut |_, rel, _| {
hits.push(rel.to_string());
true
});
assert!(hits.is_empty(), "{hits:?}");
idx.apply_test_changes(vec![Change {
root: 0,
rel: "src".into(),
kind: ChangeKind::Upsert,
}]);
hits.clear();
idx.visit_content_candidates(dir.path(), "fn main", false, &mut |_, rel, _| {
hits.push(rel.to_string());
true
});
assert!(hits.iter().any(|r| r == "src/main.rs"), "{hits:?}");
}