mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
498 lines
15 KiB
Rust
498 lines
15 KiB
Rust
use bichon_blob::{Codec, Config, Engine};
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_create_and_list_accounts() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
|
|
engine.create_account("alice").unwrap();
|
|
engine.create_account("bob").unwrap();
|
|
|
|
let accounts = engine.list_accounts();
|
|
assert!(accounts.contains(&"alice".to_string()));
|
|
assert!(accounts.contains(&"bob".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_and_read() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let key = [0xAA; 32];
|
|
let value = b"Hello, this is a test email!".to_vec();
|
|
|
|
engine
|
|
.write("alice", key, &value, Codec::Zstd)
|
|
.unwrap();
|
|
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, Some(value));
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_missing_key() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let key = [0xFF; 32];
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_delete() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let key = [0xBB; 32];
|
|
let value = b"Some email content".to_vec();
|
|
|
|
engine
|
|
.write("alice", key, &value, Codec::Zstd)
|
|
.unwrap();
|
|
engine.delete("alice", &key).unwrap();
|
|
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_delete_account() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
engine.delete_account("alice").unwrap();
|
|
|
|
let accounts = engine.list_accounts();
|
|
assert!(!accounts.contains(&"alice".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_small_value_not_compressed() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let key = [0xCC; 32];
|
|
let value = b"hi"; // Smaller than 4KB threshold
|
|
|
|
engine
|
|
.write("alice", key, value, Codec::Zstd)
|
|
.unwrap();
|
|
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, Some(value.to_vec()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_value() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let key = [0xDD; 32];
|
|
let value = vec![b'X'; 100_000]; // 100KB
|
|
|
|
engine
|
|
.write("alice", key, &value, Codec::Zstd)
|
|
.unwrap();
|
|
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, Some(value));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_keys() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let n = 100;
|
|
for i in 0..n {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
let value = format!("email number {}", i).into_bytes();
|
|
engine
|
|
.write("alice", key, &value, Codec::Zstd)
|
|
.unwrap();
|
|
}
|
|
|
|
for i in 0..n {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, Some(format!("email number {}", i).into_bytes()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_gc() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
// Write many entries
|
|
let value = vec![b'Y'; 5000];
|
|
let n = 100;
|
|
|
|
for i in 0..n {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
engine
|
|
.write("alice", key, &value, Codec::None)
|
|
.unwrap();
|
|
}
|
|
|
|
// Delete even-numbered keys
|
|
for i in (0..n).step_by(2) {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
engine.delete("alice", &key).unwrap();
|
|
}
|
|
|
|
// Run GC
|
|
let _result = engine.gc("alice").unwrap();
|
|
|
|
// Verify remaining keys still readable
|
|
for i in (1..n).step_by(2) {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, Some(value.clone()));
|
|
}
|
|
|
|
// Deleted keys should not exist
|
|
for i in (0..n).step_by(2) {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_reopen_persistence() {
|
|
let dir = TempDir::new().unwrap();
|
|
let key = [0xEE; 32];
|
|
let value = b"persistent data".to_vec();
|
|
|
|
{
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
engine
|
|
.write("alice", key, &value, Codec::Zstd)
|
|
.unwrap();
|
|
}
|
|
|
|
// Reopen
|
|
{
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
let result = engine.read("alice", &key).unwrap();
|
|
assert_eq!(result, Some(value));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_stats() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
engine
|
|
.write("alice", [1u8; 32], b"hello", Codec::None)
|
|
.unwrap();
|
|
|
|
let stats = engine.stats("alice").unwrap();
|
|
assert!(stats.total_bytes > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_write() {
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let n = 50;
|
|
let entries: Vec<_> = (0..n)
|
|
.map(|i: u64| {
|
|
let mut key = [0u8; 32];
|
|
key[0..8].copy_from_slice(&i.to_le_bytes());
|
|
let value = format!("batch email {}", i).into_bytes();
|
|
(key, value, Codec::Zstd)
|
|
})
|
|
.collect();
|
|
|
|
engine.write_batch("alice", &entries).unwrap();
|
|
|
|
for (key, value, _) in &entries {
|
|
let result = engine.read("alice", key).unwrap();
|
|
assert_eq!(result.as_ref(), Some(value));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_write_persistence() {
|
|
let dir = TempDir::new().unwrap();
|
|
let entries: Vec<_> = (0..30u64)
|
|
.map(|i| {
|
|
let mut key = [0u8; 32];
|
|
key[0..8].copy_from_slice(&i.to_le_bytes());
|
|
(key, format!("persist {}", i).into_bytes(), Codec::Zstd)
|
|
})
|
|
.collect();
|
|
|
|
{
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
engine.write_batch("alice", &entries).unwrap();
|
|
}
|
|
|
|
{
|
|
let engine = Engine::open(dir.path(), Config::default()).unwrap();
|
|
for (key, value, _) in &entries {
|
|
let result = engine.read("alice", key).unwrap();
|
|
assert_eq!(result.as_ref(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_config_rejected() {
|
|
let dir = TempDir::new().unwrap();
|
|
let mut config = Config::default();
|
|
config.lru_bucket_count = 0;
|
|
assert!(Engine::open(dir.path(), config).is_err());
|
|
|
|
let mut config = Config::default();
|
|
config.gc_deleted_ratio = 1.5;
|
|
assert!(Engine::open(dir.path(), config).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_reads() {
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Arc::new(Engine::open(dir.path(), Config::default()).unwrap());
|
|
engine.create_account("alice").unwrap();
|
|
|
|
// Write some data
|
|
for i in 0..50u32 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
engine.write("alice", key, &vec![i as u8; 1024], Codec::None).unwrap();
|
|
}
|
|
|
|
// Spawn 4 threads, each reading a different subset
|
|
let mut handles = vec![];
|
|
for t in 0..4 {
|
|
let engine = engine.clone();
|
|
handles.push(thread::spawn(move || {
|
|
for i in (t * 12)..((t + 1) * 12) {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
let read = engine.read("alice", &key).unwrap();
|
|
assert!(read.is_some(), "key {} should exist", i);
|
|
}
|
|
}));
|
|
}
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_writes_different_accounts() {
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Arc::new(Engine::open(dir.path(), Config::default()).unwrap());
|
|
|
|
for name in &["alice", "bob", "carol"] {
|
|
engine.create_account(name).unwrap();
|
|
}
|
|
|
|
let mut handles = vec![];
|
|
for (t, name) in ["alice", "bob", "carol"].iter().enumerate() {
|
|
let engine = engine.clone();
|
|
let account_name = name.to_string();
|
|
handles.push(thread::spawn(move || {
|
|
for i in 0..20 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&((t * 100 + i) as u32).to_le_bytes());
|
|
let value = vec![(t * 100 + i) as u8; 512];
|
|
engine.write(&account_name, key, &value, Codec::None).unwrap();
|
|
}
|
|
}));
|
|
}
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
|
|
// Verify all writes persisted
|
|
for (t, name) in ["alice", "bob", "carol"].iter().enumerate() {
|
|
for i in 0..20 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&((t * 100 + i) as u32).to_le_bytes());
|
|
let read = engine.read(name, &key).unwrap();
|
|
assert!(read.is_some(), "account {} key {} should exist", name, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_crash_recovery() {
|
|
let dir = TempDir::new().unwrap();
|
|
let dir_path = dir.path().to_path_buf();
|
|
|
|
// Phase 1: write data, then drop without shutdown (simulates crash)
|
|
{
|
|
let engine = Engine::open(&dir_path, Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
for i in 0..50u32 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
engine.write("alice", key, &vec![i as u8; 512], Codec::None).unwrap();
|
|
}
|
|
// Engine dropped here without calling shutdown()
|
|
}
|
|
|
|
// Phase 2: reopen — recovery should run, data should be intact
|
|
let engine = Engine::open(&dir_path, Config::default()).unwrap();
|
|
let stats = engine.stats("alice").unwrap();
|
|
assert!(stats.total_keys > 0, "recovery should preserve data");
|
|
|
|
// Verify reads work
|
|
for i in 0..50u32 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
let read = engine.read("alice", &key).unwrap();
|
|
assert!(read.is_some(), "key {} should survive crash recovery", i);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_bin_durability() {
|
|
// Verify meta.bin has valid CRC and can be read after a write cycle.
|
|
let dir = TempDir::new().unwrap();
|
|
let dir_path = dir.path().to_path_buf();
|
|
|
|
{
|
|
let engine = Engine::open(&dir_path, Config::default()).unwrap();
|
|
engine.create_account("alice").unwrap();
|
|
|
|
let key = [0x42u8; 32];
|
|
engine.write("alice", key, b"durable", Codec::None).unwrap();
|
|
}
|
|
// Engine dropped → shutdown() called → meta saved via write_bin (with fsync)
|
|
|
|
// Verify meta.bin exists and has valid CRC
|
|
let meta_path = dir_path
|
|
.join("accounts")
|
|
.join("alice")
|
|
.join("meta.bin");
|
|
assert!(meta_path.exists(), "meta.bin should exist after clean shutdown");
|
|
|
|
let data = std::fs::read(&meta_path).unwrap();
|
|
assert!(data.len() >= 8, "meta.bin should have at least 8 bytes (crc + version)");
|
|
|
|
let stored_crc = u32::from_le_bytes(data[0..4].try_into().unwrap());
|
|
assert_ne!(stored_crc, 0, "stored CRC should be non-zero");
|
|
|
|
// Reopen and verify data is intact
|
|
let engine = Engine::open(&dir_path, Config::default()).unwrap();
|
|
let read = engine.read("alice", &[0x42u8; 32]).unwrap();
|
|
assert_eq!(read, Some(b"durable".to_vec()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_gc_concurrent_with_writes() {
|
|
// GC should not lose entries that are written concurrently.
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::thread;
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
let engine = Arc::new(Engine::open(dir.path(), Config::default()).unwrap());
|
|
engine.create_account("alice").unwrap();
|
|
|
|
// Pre-fill: write enough to trigger eventual GC
|
|
let big_value = vec![b'X'; 8192];
|
|
for i in 0..500u32 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
engine.write("alice", key, &big_value, Codec::None).unwrap();
|
|
}
|
|
|
|
// Delete some to create GC candidates
|
|
for i in 0..250u32 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
engine.delete("alice", &key).unwrap();
|
|
}
|
|
|
|
let running = Arc::new(AtomicBool::new(true));
|
|
let engine_gc = engine.clone();
|
|
let running_gc = running.clone();
|
|
|
|
// Thread 1: run GC in a loop
|
|
let gc_handle = thread::spawn(move || {
|
|
while running_gc.load(Ordering::Relaxed) {
|
|
let _ = engine_gc.gc("alice");
|
|
thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
});
|
|
|
|
// Thread 2: keep writing new entries
|
|
let engine_write = engine.clone();
|
|
let running_write = running.clone();
|
|
let write_handle = thread::spawn(move || {
|
|
let mut counter = 10000u32;
|
|
while running_write.load(Ordering::Relaxed) {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&counter.to_le_bytes());
|
|
engine_write
|
|
.write("alice", key, &vec![counter as u8; 256], Codec::None)
|
|
.unwrap();
|
|
counter += 1;
|
|
}
|
|
counter
|
|
});
|
|
|
|
// Let them race for a bit
|
|
thread::sleep(std::time::Duration::from_millis(500));
|
|
running.store(false, Ordering::Relaxed);
|
|
|
|
gc_handle.join().unwrap();
|
|
let final_counter = write_handle.join().unwrap();
|
|
|
|
// All written entries must be readable
|
|
let mut missing = 0;
|
|
for i in 0..500u32 {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
if engine.read("alice", &key).unwrap().is_none() {
|
|
// Entries 0..250 were deleted, they should be gone
|
|
if i >= 250 {
|
|
missing += 1;
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(missing, 0, "pre-existing entries should survive concurrent GC");
|
|
|
|
// Entries written during the race should be readable
|
|
for i in 10000..final_counter {
|
|
let mut key = [0u8; 32];
|
|
key[0..4].copy_from_slice(&i.to_le_bytes());
|
|
let read = engine.read("alice", &key).unwrap();
|
|
assert!(read.is_some(), "concurrently written key {} should exist after GC", i);
|
|
}
|
|
}
|