use super::*;
fn tempdir() -> tempfile::TempDir {
tempfile::tempdir().expect("tempdir")
}
#[test]
fn snapshot_written_by_the_daemon_is_read_in_place() {
let dir = tempdir();
let snap = dir.path().join(SNAPSHOT_FILENAME);
std::fs::write(
&snap,
br#"[{"doc_id":"drawer-a","text":"the quick brown fox"},
{"doc_id":"drawer-b","text":"lazy dog sleeping"}]"#,
)
.unwrap();
let idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 2);
assert!(!idx.is_dirty(), "a freshly loaded snapshot is not dirty");
let hits = idx.search("fox", 10);
assert_eq!(hits.len(), 1, "got: {hits:?}");
assert_eq!(hits[0].doc_id, "drawer-a");
assert!(
idx.missing_docs(&["drawer-a".into(), "drawer-b".into()])
.is_empty(),
"both daemon-written documents must be present by id"
);
}
#[test]
fn flush_round_trips() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("x", "one two three");
idx.flush().unwrap();
let raw = std::fs::read_to_string(idx.snapshot_path()).unwrap();
assert!(raw.contains("\"doc_id\":\"x\""), "got: {raw}");
assert!(raw.contains("\"text\":\"one two three\""), "got: {raw}");
let reopened = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(reopened.doc_count(), 1);
assert!(reopened.search("two", 5).iter().any(|h| h.doc_id == "x"));
}
#[test]
fn search_returns_hits() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("doc1", "authentication login password");
idx.index_doc("doc2", "rendering ui components");
let hits = idx.search("authentication", 5);
assert_eq!(hits.len(), 1, "got: {hits:?}");
assert_eq!(hits[0].doc_id, "doc1");
assert!(hits[0].score > 0.0);
}
#[test]
fn search_clamps_a_zero_top_k() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("doc1", "alpha");
assert_eq!(idx.search("alpha", 0).len(), 1);
}
#[test]
fn index_doc_marks_dirty() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert!(!idx.is_dirty());
idx.index_doc("d", "hello");
assert!(idx.is_dirty());
idx.flush().unwrap();
assert!(!idx.is_dirty());
}
#[test]
fn delete_doc_removes_and_marks_dirty() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("d", "alpha beta");
idx.flush().unwrap();
assert!(idx.delete_doc("d"));
assert!(idx.is_dirty());
assert_eq!(idx.doc_count(), 0);
assert!(idx.search("alpha", 5).is_empty());
idx.flush().unwrap();
assert!(!idx.delete_doc("never-existed"));
assert!(!idx.is_dirty(), "an unknown-id delete must not re-dirty");
}
#[test]
fn stats_track_docs_and_bytes() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 0);
assert_eq!(idx.total_text_bytes(), 0);
idx.index_doc("a", "hello");
idx.index_doc("b", "world!");
assert_eq!(idx.doc_count(), 2);
assert_eq!(idx.total_text_bytes(), 11);
assert!(idx.delete_doc("a"));
assert_eq!(idx.doc_count(), 1);
assert_eq!(idx.total_text_bytes(), 6);
}
#[test]
fn missing_docs_answers_by_identity_not_count() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("a", "alpha");
idx.index_doc("stale", "a drawer that was deleted from the palace");
let asked = vec!["a".to_string(), "b".to_string()];
assert_eq!(
idx.doc_count(),
asked.len(),
"precondition: the count comparison is satisfied and still wrong"
);
assert_eq!(idx.missing_docs(&asked), vec!["b".to_string()]);
idx.index_doc("b", "beta");
assert!(idx.missing_docs(&asked).is_empty());
assert!(
idx.missing_docs(&[]).is_empty(),
"an empty request is trivially covered"
);
}
#[test]
fn load_recovers_from_a_corrupt_snapshot() {
let dir = tempdir();
std::fs::write(dir.path().join(SNAPSHOT_FILENAME), b"not valid json").unwrap();
let idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 0);
}
#[test]
#[cfg(unix)]
fn load_propagates_an_unreadable_snapshot() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir();
let snap = dir.path().join(SNAPSHOT_FILENAME);
std::fs::write(&snap, br#"[{"doc_id":"a","text":"alpha"}]"#).unwrap();
std::fs::set_permissions(&snap, std::fs::Permissions::from_mode(0o000)).unwrap();
let result = PalaceBm25Index::load_or_create(dir.path());
std::fs::set_permissions(&snap, std::fs::Permissions::from_mode(0o644)).unwrap();
if unsafe { libc::geteuid() } == 0 {
return;
}
assert!(
result.is_err(),
"an unreadable snapshot must propagate, not read as an empty corpus"
);
}
#[test]
#[cfg(unix)]
fn a_failed_flush_leaves_the_index_dirty() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
return;
}
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("a", "alpha");
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
let result = idx.flush();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(result.is_err(), "a read-only directory must fail the flush");
assert!(
idx.is_dirty(),
"a failed flush must leave the index dirty so the next tick retries"
);
idx.flush().expect("the retry must succeed");
assert!(!idx.is_dirty());
}