use std::io::Cursor;
use triviumdb::storage::wal::{SyncMode, Wal, WalEntry};
fn tmp_db(name: &str) -> String {
let dir = std::env::temp_dir().join("triviumdb_test");
std::fs::create_dir_all(&dir).ok();
dir.join(format!("unit_wal_{}", name))
.to_string_lossy()
.to_string()
}
fn cleanup(path: &str) {
for ext in &["", ".wal", ".vec", ".lock", ".flush_ok"] {
std::fs::remove_file(format!("{}{}", path, ext)).ok();
}
}
#[test]
fn wal_append_和_read_entries_往返() {
let path = tmp_db("roundtrip");
cleanup(&path);
{
let mut wal = Wal::open(&path).unwrap();
wal.append(&WalEntry::Insert::<f32> {
id: 1,
vector: vec![1.0, 2.0, 3.0],
payload: r#"{"name":"alice"}"#.to_string(),
})
.unwrap();
wal.append(&WalEntry::Insert::<f32> {
id: 2,
vector: vec![4.0, 5.0, 6.0],
payload: r#"{"name":"bob"}"#.to_string(),
})
.unwrap();
}
let (entries, _) = Wal::read_entries::<f32>(&path).unwrap();
assert_eq!(entries.len(), 2);
match &entries[0] {
WalEntry::Insert {
id,
vector,
payload,
} => {
assert_eq!(*id, 1);
assert_eq!(*vector, vec![1.0, 2.0, 3.0]);
assert!(payload.contains("alice"));
}
_ => panic!("第一条应为 Insert"),
}
cleanup(&path);
}
#[test]
fn wal_所有Entry变体往返() {
let path = tmp_db("all_variants");
cleanup(&path);
{
let mut wal = Wal::open(&path).unwrap();
wal.append(&WalEntry::Insert::<f32> {
id: 1,
vector: vec![1.0],
payload: "{}".to_string(),
})
.unwrap();
wal.append(&WalEntry::Link::<f32> {
src: 1,
dst: 2,
label: "knows".to_string(),
weight: 0.5,
})
.unwrap();
wal.append(&WalEntry::Delete::<f32> { id: 3 }).unwrap();
wal.append(&WalEntry::Unlink::<f32> { src: 1, dst: 2 })
.unwrap();
wal.append(&WalEntry::UpdatePayload::<f32> {
id: 1,
payload: r#"{"updated":true}"#.to_string(),
})
.unwrap();
wal.append(&WalEntry::UpdateVector::<f32> {
id: 1,
vector: vec![9.0],
})
.unwrap();
}
let (entries, _) = Wal::read_entries::<f32>(&path).unwrap();
assert_eq!(entries.len(), 6);
cleanup(&path);
}
#[test]
fn wal_append_batch_事务完整性() {
let path = tmp_db("batch");
cleanup(&path);
{
let mut wal = Wal::open(&path).unwrap();
let entries = vec![
WalEntry::Insert::<f32> {
id: 10,
vector: vec![1.0],
payload: "{}".to_string(),
},
WalEntry::Insert::<f32> {
id: 11,
vector: vec![2.0],
payload: "{}".to_string(),
},
];
wal.append_batch(42, &entries).unwrap();
}
let (entries, _) = Wal::read_entries::<f32>(&path).unwrap();
assert_eq!(entries.len(), 2);
cleanup(&path);
}
#[test]
fn wal_clear_后读取为空() {
let path = tmp_db("clear");
cleanup(&path);
let mut wal = Wal::open(&path).unwrap();
wal.append(&WalEntry::Insert::<f32> {
id: 1,
vector: vec![1.0],
payload: "{}".to_string(),
})
.unwrap();
wal.clear().unwrap();
let (entries, _) = Wal::read_entries::<f32>(&path).unwrap();
assert!(entries.is_empty(), "clear 后应无条目");
wal.append(&WalEntry::Insert::<f32> {
id: 2,
vector: vec![2.0],
payload: "{}".to_string(),
})
.unwrap();
drop(wal);
let (entries, _) = Wal::read_entries::<f32>(&path).unwrap();
assert_eq!(entries.len(), 1);
cleanup(&path);
}
#[test]
fn wal_sync_mode_切换() {
let path = tmp_db("sync_mode");
cleanup(&path);
let mut wal = Wal::open_with_sync(&path, SyncMode::Full).unwrap();
wal.append(&WalEntry::Insert::<f32> {
id: 1,
vector: vec![1.0],
payload: "{}".to_string(),
})
.unwrap();
wal.set_sync_mode(SyncMode::Off);
wal.append(&WalEntry::Insert::<f32> {
id: 2,
vector: vec![2.0],
payload: "{}".to_string(),
})
.unwrap();
wal.flush_writer();
drop(wal);
let (entries, _) = Wal::read_entries::<f32>(&path).unwrap();
assert_eq!(entries.len(), 2);
cleanup(&path);
}
#[test]
fn wal_needs_recovery_空文件() {
let path = tmp_db("needs_rec_empty");
cleanup(&path);
assert!(!Wal::needs_recovery(&path));
let wal = Wal::open(&path).unwrap();
drop(wal);
assert!(!Wal::needs_recovery(&path), "空 WAL 不需要恢复");
cleanup(&path);
}
#[test]
fn wal_needs_recovery_非空文件() {
let path = tmp_db("needs_rec_data");
cleanup(&path);
let mut wal = Wal::open(&path).unwrap();
wal.append(&WalEntry::Insert::<f32> {
id: 1,
vector: vec![1.0],
payload: "{}".to_string(),
})
.unwrap();
drop(wal);
assert!(Wal::needs_recovery(&path), "非空 WAL 应该需要恢复");
cleanup(&path);
}
#[test]
fn wal_crc_错误时停止恢复() {
let entry = WalEntry::Insert::<f32> {
id: 1,
vector: vec![1.0, 2.0],
payload: "{}".to_string(),
};
let data = bincode::serialize(&entry).unwrap();
let crc = crc32fast::hash(&data);
let mut buf = Vec::new();
buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
buf.extend_from_slice(&data);
buf.extend_from_slice(&crc.to_le_bytes());
buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
buf.extend_from_slice(&data);
buf.extend_from_slice(&0xDEADBEEFu32.to_le_bytes());
let (entries, _) = Wal::read_entries_from_reader::<f32>(Cursor::new(&buf)).unwrap();
assert_eq!(entries.len(), 1, "CRC 错误后应只恢复第一条");
}
#[test]
fn wal_len过大_合理性检查() {
let mut buf = Vec::new();
buf.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes());
buf.extend_from_slice(&[0xAA; 100]);
let (entries, _) = Wal::read_entries_from_reader::<f32>(Cursor::new(&buf)).unwrap();
assert!(entries.is_empty(), "超大 len 应触发安全停止");
}