#![cfg(feature = "durability")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::Path;
use proptest::prelude::*;
use txn_db::{Db, Timestamp};
fn read(db: &Db, key: &[u8]) -> Option<u8> {
db.snapshot().get(key).unwrap().map(|bytes| bytes[0])
}
fn commit_one(db: &Db, key: u8, value: u8) {
let mut tx = db.begin();
tx.put(vec![key], vec![value]);
tx.commit().unwrap();
}
#[test]
fn test_committed_transactions_survive_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("txn.wal");
{
let db = Db::open(&path).unwrap();
for i in 0..16u8 {
commit_one(&db, i, i.wrapping_mul(3));
}
}
let db = Db::open(&path).unwrap();
for i in 0..16u8 {
assert_eq!(read(&db, &[i]), Some(i.wrapping_mul(3)));
}
}
#[test]
fn test_uncommitted_work_is_not_durable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("txn.wal");
{
let db = Db::open(&path).unwrap();
commit_one(&db, 1, 10);
let mut rolled_back = db.begin();
rolled_back.put(vec![2], vec![20]);
rolled_back.rollback();
let mut dropped = db.begin();
dropped.put(vec![3], vec![30]);
drop(dropped);
}
let db = Db::open(&path).unwrap();
assert_eq!(read(&db, &[1]), Some(10)); assert_eq!(read(&db, &[2]), None); assert_eq!(read(&db, &[3]), None); }
#[test]
fn test_tombstones_survive_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("txn.wal");
{
let db = Db::open(&path).unwrap();
commit_one(&db, 7, 99);
let mut tx = db.begin();
tx.delete(vec![7]);
tx.commit().unwrap();
}
let db = Db::open(&path).unwrap();
assert_eq!(read(&db, &[7]), None);
}
#[test]
fn test_timestamps_continue_after_recovery() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("txn.wal");
let last = {
let db = Db::open(&path).unwrap();
commit_one(&db, 1, 1);
commit_one(&db, 2, 2);
db.last_committed()
};
assert!(last > Timestamp::ZERO);
let db = Db::open(&path).unwrap();
assert_eq!(db.last_committed(), last);
let mut tx = db.begin();
tx.put(vec![3], vec![3]);
let next = tx.commit().unwrap();
assert!(next > last);
}
#[test]
fn test_reopen_empty_log_is_empty_database() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("txn.wal");
let db = Db::open(&path).unwrap();
assert_eq!(db.last_committed(), Timestamp::ZERO);
assert_eq!(read(&db, &[0]), None);
}
fn open_truncated(source: &Path, len: usize, dest: &Path) -> Db {
let bytes = std::fs::read(source).unwrap();
let len = len.min(bytes.len());
std::fs::write(dest, &bytes[..len]).unwrap();
Db::open(dest).unwrap()
}
proptest! {
#[test]
fn recovery_from_truncated_log_is_a_clean_prefix(
n in 1u8..20,
cut in any::<prop::sample::Index>(),
) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("txn.wal");
{
let db = Db::open(&path).unwrap();
for i in 0..n {
commit_one(&db, i, i.wrapping_add(1));
}
}
let total = std::fs::metadata(&path).unwrap().len() as usize;
let len = cut.index(total + 1);
let recovered = open_truncated(&path, len, &dir.path().join("recovered.wal"));
let mut k = 0u8;
while k < n && read(&recovered, &[k]) == Some(k.wrapping_add(1)) {
k += 1;
}
for i in k..n {
prop_assert_eq!(read(&recovered, &[i]), None);
}
}
}