#![cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
use regolith::env::{Env, WriteMode};
use regolith::{Db, MemEnv, Options, WriteBatch};
fn opts(env: &Arc<MemEnv>) -> Options {
Options {
env: env.clone(),
max_background_compactions: 0,
..Options::default()
}
}
#[test]
fn a_database_on_a_memory_env_reopens_with_every_acknowledged_write() {
let env = Arc::new(MemEnv::new());
{
let db = Db::open("/reopen", opts(&env)).expect("first open");
for i in 0..200u32 {
db.put(format!("k{i:04}").as_bytes(), format!("v{i}").as_bytes())
.expect("put");
}
let mut batch = WriteBatch::new();
batch.put(b"batched", b"yes");
batch.delete(b"k0000");
db.write(batch).expect("batch");
db.close().expect("close");
}
let db = Db::open("/reopen", opts(&env)).expect(
"reopening a database on a non-default Env must work; a NotFound here means some \
path took std::fs instead of the Env and looked on the real filesystem",
);
assert_eq!(db.get(b"k0001").expect("get").as_deref(), Some(&b"v1"[..]));
assert_eq!(
db.get(b"batched").expect("get").as_deref(),
Some(&b"yes"[..])
);
assert_eq!(
db.get(b"k0000").expect("get"),
None,
"the batched delete must survive"
);
assert_eq!(db.scan(None, None).expect("scan").len(), 200);
db.close().expect("close");
}
#[test]
fn a_memory_env_database_replays_its_wal_after_an_unclean_close() {
let env = Arc::new(MemEnv::new());
{
let db = Db::open("/unclean", opts(&env)).expect("first open");
for i in 0..50u32 {
db.put(format!("u{i:04}").as_bytes(), b"v").expect("put");
}
drop(db);
}
let db = Db::open("/unclean", opts(&env)).expect("reopen after an unclean close");
for i in 0..50u32 {
assert_eq!(
db.get(format!("u{i:04}").as_bytes())
.expect("get")
.as_deref(),
Some(&b"v"[..]),
"u{i:04} did not survive WAL replay through the Env",
);
}
db.close().expect("close");
}
#[test]
fn a_memory_env_database_reopens_when_its_newest_wal_ends_mid_record() {
let env = Arc::new(MemEnv::new());
{
let db = Db::open("/torn", opts(&env)).expect("first open");
for i in 0..40u32 {
db.put(format!("t{i:04}").as_bytes(), b"value")
.expect("put");
}
drop(db);
}
let wal = newest_wal(&env, "/torn");
let full = Env::read(&*env, &wal).expect("read wal");
assert!(full.len() > 24, "the log needs records to cut into");
let cut = full.len() - 7;
write_all(&env, &wal, &full[..cut]);
let db = Db::open("/torn", opts(&env)).expect(
"a torn tail on the newest log is a crash artifact, not an unopenable database; a \
failure here means the torn-tail classifier read the real filesystem instead of \
the Env",
);
let survived = (0..40u32)
.filter(|i| {
db.get(format!("t{i:04}").as_bytes())
.expect("get")
.is_some()
})
.count();
assert!(
survived >= 39,
"a cut inside the last record must not lose the records before it, {survived}/40 survived",
);
db.close().expect("close");
}
fn newest_wal(env: &Arc<MemEnv>, dir: &str) -> std::path::PathBuf {
let mut logs: Vec<_> = Env::read_dir(&**env, std::path::Path::new(&format!("{dir}/wal")))
.expect("read_dir wal")
.into_iter()
.map(|e| e.path)
.filter(|p| p.extension().is_some_and(|x| x == "log"))
.collect();
logs.sort();
logs.pop().expect("at least one wal file")
}
fn write_all(env: &Arc<MemEnv>, path: &std::path::Path, bytes: &[u8]) {
let mut f = Env::open_write(&**env, path, WriteMode::Truncate).expect("open_write");
f.write_all(bytes).expect("write");
f.sync_all().expect("sync");
}