use ix::builder::Builder;
use ix::executor::{Executor, QueryOptions};
use ix::planner::Planner;
use ix::reader::Reader;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_c1_delta_roundtrip_after_rebuild() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::write(root.join("a.txt"), "needle in haystack").unwrap();
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix/shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan = Planner::plan("needle", false);
let (matches, _) = executor.execute(&plan, &QueryOptions::default()).unwrap();
assert_eq!(
matches.len(),
1,
"C1: Initial build should find needle in a.txt"
);
fs::write(root.join("a.txt"), "needle transformed").unwrap();
fs::write(root.join("b.txt"), "another needle here").unwrap();
let mut builder2 = Builder::new(root).unwrap();
builder2.build().unwrap();
let reader2 = Reader::open(&index_path).unwrap();
let mut executor2 = Executor::new(&reader2);
let (matches2, _) = executor2.execute(&plan, &QueryOptions::default()).unwrap();
assert_eq!(
matches2.len(),
2,
"C1: Rebuild should find needle in both files"
);
}
#[test]
fn test_c1_delta_tombstone_removes_file() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::write(root.join("explicit.rs"), "fn remove_me() {}").unwrap();
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix/shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan = Planner::plan("remove_me", false);
let (matches, _) = executor.execute(&plan, &QueryOptions::default()).unwrap();
assert_eq!(matches.len(), 1, "C1: File must exist before tombstone");
fs::remove_file(root.join("explicit.rs")).unwrap();
let mut builder2 = Builder::new(root).unwrap();
builder2.build().unwrap();
let reader2 = Reader::open(&index_path).unwrap();
let mut executor2 = Executor::new(&reader2);
let (matches2, _) = executor2.execute(&plan, &QueryOptions::default()).unwrap();
assert_eq!(
matches2.len(),
0,
"C1: Tombstone should remove deleted file from search results"
);
}
#[test]
fn test_c3_cdx_trigram_identity_at_block_boundaries() {
let dir = tempdir().unwrap();
let root = dir.path();
let mut content = String::new();
for i in 0u8..=255u8 {
content.push_str(&format!("{i:03} "));
content.push_str("abc"); }
fs::write(root.join("cdx_test.txt"), &content).unwrap();
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix/shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan = Planner::plan("abc", false);
let (matches, stats) = executor.execute(&plan, &QueryOptions::default()).unwrap();
assert!(
!matches.is_empty(),
"C3: Trigrams must survive CDX roundtrip"
);
assert!(
stats.trigrams_queried > 0,
"C3: CDX lookup must be exercised"
);
assert_eq!(
stats.files_failed_verify, 0,
"C3: No files should fail verification during CDX lookup"
);
}
#[test]
fn test_c3_cdx_block_index_binary_search_correct() {
let dir = tempdir().unwrap();
let root = dir.path();
let keywords: Vec<String> = (0..64).map(|i| format!("key_{i:04}")).collect::<Vec<_>>();
for kw in &keywords {
fs::write(
root.join(format!("{kw}.txt")),
format!("line with {kw} inside"),
)
.unwrap();
}
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix/shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
for kw in ["key_0000", "key_0063"] {
let plan = Planner::plan(kw, false);
let (matches, _) = executor.execute(&plan, &QueryOptions::default()).unwrap();
assert_eq!(
matches.len(),
1,
"C3: Binary search must locate edge key '{kw}' exactly"
);
}
let plan = Planner::plan("zzz_not_found", false);
let (matches, _) = executor.execute(&plan, &QueryOptions::default()).unwrap();
assert!(
matches.is_empty(),
"C3: Non-existent key must return empty results"
);
}
#[test]
fn test_c4_build_sequence_independent_results() {
let dir = tempdir().unwrap();
let root = dir.path();
let file_a = "alpha appears in file_a.txt";
let file_b = "beta appears in file_b.txt";
fs::write(root.join("a.txt"), file_a).unwrap();
fs::write(root.join("b.txt"), file_b).unwrap();
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix/shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan_a = Planner::plan("alpha", false);
let (matches_a, _) = executor.execute(&plan_a, &QueryOptions::default()).unwrap();
let names_a: std::collections::BTreeSet<&str> = matches_a
.iter()
.map(|m| m.file_path.file_name().unwrap().to_str().unwrap())
.collect();
let plan_b = Planner::plan("beta", false);
let (matches_b, _) = executor.execute(&plan_b, &QueryOptions::default()).unwrap();
let names_b: std::collections::BTreeSet<&str> = matches_b
.iter()
.map(|m| m.file_path.file_name().unwrap().to_str().unwrap())
.collect();
assert_eq!(names_a, ["a.txt"].iter().copied().collect());
assert_eq!(names_b, ["b.txt"].iter().copied().collect());
fs::write(root.join("c.txt"), "alpha and beta in new file").unwrap();
let mut builder2 = Builder::new(root).unwrap();
builder2.build().unwrap();
let reader2 = Reader::open(&index_path).unwrap();
let mut executor2 = Executor::new(&reader2);
let plan_alpha = Planner::plan("alpha", false);
let (matches, _) = executor2
.execute(&plan_alpha, &QueryOptions::default())
.unwrap();
assert_eq!(
matches.len(),
2,
"C4: Incremental build should find alpha in a.txt and c.txt"
);
}
#[test]
fn test_c1_empty_index_contract() {
let dir = tempdir().unwrap();
let root = dir.path();
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix/shard.ix");
assert!(index_path.exists(), "C1: Empty index file must exist");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan = Planner::plan("anything", false);
let (matches, _) = executor.execute(&plan, &QueryOptions::default()).unwrap();
assert!(
matches.is_empty(),
"C1: Empty index must return zero matches"
);
let mut builder2 = Builder::new(root).unwrap();
builder2.build().unwrap();
let reader2 = Reader::open(&index_path).unwrap();
let mut executor2 = Executor::new(&reader2);
let (matches2, _) = executor2.execute(&plan, &QueryOptions::default()).unwrap();
assert!(
matches2.is_empty(),
"C1: Empty index rebuild must be idempotent"
);
}
#[test]
fn test_c3_delta_only_keyword_included_in_results() {
use std::fs::File;
use std::io::Write;
let dir = tempdir().unwrap();
let root = dir.path();
let index_dir = root.join(".ix");
std::fs::create_dir(&index_dir).unwrap();
let a_path = root.join("a.txt");
let mut a = File::create(&a_path).unwrap();
writeln!(a, "alpha base file content").unwrap();
drop(a);
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = index_dir.join("shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan_alpha = Planner::plan("alpha", false);
let (matches, _) = executor
.execute(&plan_alpha, &QueryOptions::default())
.unwrap();
assert_eq!(matches.len(), 1, "base index should find alpha in a.txt");
let b_path = root.join("b.txt");
let mut b = File::create(&b_path).unwrap();
writeln!(b, "xyzzy delta_only_keyword unique").unwrap();
drop(b);
let mut builder2 = Builder::new(root).unwrap();
builder2.build().unwrap();
let reader2 = Reader::open(&index_path).unwrap();
let mut executor2 = Executor::new(&reader2);
let plan_delta = Planner::plan("delta_only_keyword", false);
let (matches2, _) = executor2
.execute(&plan_delta, &QueryOptions::default())
.unwrap();
assert!(
!matches2.is_empty(),
"C3: delta-only keyword should return results from delta file"
);
assert!(
matches2.iter().any(|m| m.file_path == b_path),
"C3: result should include the delta file b.txt, got: {:?}",
matches2.iter().map(|m| &m.file_path).collect::<Vec<_>>()
);
}
#[test]
fn test_c3_regex_delta_only_pattern_included_in_results() {
use std::fs::File;
use std::io::Write;
let dir = tempdir().unwrap();
let root = dir.path();
let index_dir = root.join(".ix");
std::fs::create_dir(&index_dir).unwrap();
let a_path = root.join("a.txt");
let mut a = File::create(&a_path).unwrap();
writeln!(a, "plain text line").unwrap();
drop(a);
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = index_dir.join("shard.ix");
let reader = Reader::open(&index_path).unwrap();
let mut executor = Executor::new(&reader);
let plan_plain = Planner::plan("plain", false);
let (matches, _) = executor
.execute(&plan_plain, &QueryOptions::default())
.unwrap();
assert_eq!(matches.len(), 1, "base should find plain in a.txt");
let c_path = root.join("c.txt");
let mut c = File::create(&c_path).unwrap();
writeln!(c, "ERROR: timeout exceeded at line 42").unwrap();
drop(c);
let mut builder2 = Builder::new(root).unwrap();
builder2.build().unwrap();
let reader2 = Reader::open(&index_path).unwrap();
let mut executor2 = Executor::new(&reader2);
let plan_regex = Planner::plan("ERROR.*tim", true);
let (matches2, _) = executor2
.execute(&plan_regex, &QueryOptions::default())
.unwrap();
assert!(
!matches2.is_empty(),
"C3: regex matching only delta content must return results"
);
assert!(
matches2.iter().any(|m| m.file_path == c_path),
"C3: regex result should include delta file c.txt, got: {:?}",
matches2.iter().map(|m| &m.file_path).collect::<Vec<_>>()
);
}