#[cfg(test)]
mod tests {
use crate::sstable::{self, GetResult, PointEntry, RangeTombstone, Record, SSTable};
use std::fs;
use tempfile::TempDir;
use tracing::Level;
use tracing_subscriber::fmt::Subscriber;
fn init_tracing() {
let _ = Subscriber::builder()
.with_max_level(Level::TRACE)
.try_init();
}
fn point(key: &[u8], value: &[u8], lsn: u64, timestamp: u64) -> PointEntry {
PointEntry {
key: key.to_vec(),
value: Some(value.to_vec()),
lsn,
timestamp,
}
}
#[test]
fn get_nonexistent_key_returns_not_found() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("sst_notfound.bin");
let points = vec![
point(b"a", b"1", 10, 100),
point(b"c", b"3", 11, 101),
point(b"e", b"5", 12, 102),
];
let ranges: Vec<RangeTombstone> = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&path)
.build(points.into_iter(), pt_count, ranges.into_iter(), rt_count)
.unwrap();
let sst = SSTable::open(&path).unwrap();
assert_eq!(sst.get(b"b").unwrap(), GetResult::NotFound);
assert_eq!(sst.get(b"z").unwrap(), GetResult::NotFound);
}
#[test]
fn bloom_filter_absent_key_returns_not_found() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("sst_bloom.bin");
let points = vec![
point(b"apple", b"red", 1, 100),
point(b"banana", b"yellow", 2, 101),
point(b"cherry", b"dark-red", 3, 102),
];
let ranges: Vec<RangeTombstone> = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&path)
.build(points.into_iter(), pt_count, ranges.into_iter(), rt_count)
.unwrap();
let sst = SSTable::open(&path).unwrap();
assert_eq!(sst.get(b"dragonfruit").unwrap(), GetResult::NotFound);
assert_eq!(sst.get(b"elderberry").unwrap(), GetResult::NotFound);
assert_eq!(sst.get(b"zebra_fruit").unwrap(), GetResult::NotFound);
}
#[test]
fn open_corrupted_file_fails() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("sst_corrupt.bin");
let points = vec![point(b"a", b"1", 1, 100), point(b"b", b"2", 2, 101)];
let ranges: Vec<RangeTombstone> = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&path)
.build(points.into_iter(), pt_count, ranges.into_iter(), rt_count)
.unwrap();
let mut bytes = fs::read(&path).unwrap();
bytes[4] ^= 0xFF;
bytes[5] ^= 0xFF;
bytes[6] ^= 0xFF;
fs::write(&path, bytes).unwrap();
let result = SSTable::open(&path);
assert!(result.is_err(), "Opening a corrupted SSTable should fail");
}
#[test]
fn multi_block_get_and_scan() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("sst_multiblock.bin");
let num_entries = 500;
let points: Vec<PointEntry> = (0..num_entries)
.map(|i| {
let key = format!("key_{:06}", i).into_bytes();
let value = format!("value_{:06}_padding_{}", i, "X".repeat(50)).into_bytes();
point(&key, &value, i as u64 + 1, (i as u64 + 1) * 100)
})
.collect();
let ranges: Vec<RangeTombstone> = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&path)
.build(points.into_iter(), pt_count, ranges.into_iter(), rt_count)
.unwrap();
let sst = SSTable::open(&path).unwrap();
assert!(
sst.index.len() >= 2,
"Expected at least 2 index entries (multiple blocks), got {}",
sst.index.len()
);
let r = sst.get(b"key_000000").unwrap();
assert!(
matches!(r, GetResult::Put { .. }),
"First key should be found"
);
let mid = format!("key_{:06}", num_entries / 2).into_bytes();
let r = sst.get(&mid).unwrap();
assert!(
matches!(r, GetResult::Put { .. }),
"Middle key should be found"
);
let last = format!("key_{:06}", num_entries - 1).into_bytes();
let r = sst.get(&last).unwrap();
assert!(
matches!(r, GetResult::Put { .. }),
"Last key should be found"
);
assert_eq!(sst.get(b"key_999999").unwrap(), GetResult::NotFound);
let scanned: Vec<Record> = sst.scan(b"key_", b"key_\xff").unwrap().collect();
assert_eq!(
scanned.len(),
num_entries,
"Scan should return all {} entries",
num_entries
);
for (i, entry) in scanned.iter().enumerate().take(num_entries) {
let expected_key = format!("key_{:06}", i).into_bytes();
match entry {
Record::Put { key, .. } => {
assert_eq!(key, &expected_key, "Entry {} should be key_{:06}", i, i);
}
other => panic!("Expected Put at index {}, got {:?}", i, other),
}
}
}
#[test]
fn open_truncated_file_fails() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("sst_truncated.bin");
fs::write(&path, [0u8; 10]).unwrap();
let result = SSTable::open(&path);
assert!(result.is_err(), "Opening a truncated SSTable should fail");
}
#[test]
fn open_wrong_magic_fails() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("sst_bad_magic.bin");
let points = vec![point(b"a", b"1", 1, 100)];
let ranges: Vec<RangeTombstone> = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&path)
.build(points.into_iter(), pt_count, ranges.into_iter(), rt_count)
.unwrap();
let mut bytes = fs::read(&path).unwrap();
bytes[0] = b'X';
bytes[1] = b'X';
bytes[2] = b'X';
bytes[3] = b'X';
fs::write(&path, bytes).unwrap();
let result = SSTable::open(&path);
assert!(
result.is_err(),
"Opening an SSTable with wrong magic should fail"
);
}
}