#[cfg(feature = "var-collections")]
use armdb::VarTree;
use armdb::{Config, ConstTree, MigrateAction, WriteHook};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use tempfile::tempdir;
#[derive(Clone, Debug, PartialEq)]
struct HookEvent {
key: [u8; 8],
old: Option<Vec<u8>>,
new: Option<Vec<u8>>,
}
type Events = Arc<Mutex<Vec<HookEvent>>>;
fn take_events(events: &Events) -> Vec<HookEvent> {
std::mem::take(&mut *events.lock())
}
struct TrackingHook {
events: Events,
}
impl TrackingHook {
fn new(events: Events) -> Self {
Self { events }
}
}
impl WriteHook<[u8; 8]> for TrackingHook {
const NEEDS_OLD_VALUE: bool = true;
fn on_write(&self, key: &[u8; 8], old: Option<&[u8]>, new: Option<&[u8]>) {
self.events.lock().push(HookEvent {
key: *key,
old: old.map(|v| v.to_vec()),
new: new.map(|v| v.to_vec()),
});
}
}
#[cfg(feature = "var-collections")]
struct NoOldHook {
events: Events,
}
#[cfg(feature = "var-collections")]
impl NoOldHook {
fn new(events: Events) -> Self {
Self { events }
}
}
#[cfg(feature = "var-collections")]
impl WriteHook<[u8; 8]> for NoOldHook {
const NEEDS_OLD_VALUE: bool = false;
fn on_write(&self, key: &[u8; 8], old: Option<&[u8]>, new: Option<&[u8]>) {
self.events.lock().push(HookEvent {
key: *key,
old: old.map(|v| v.to_vec()),
new: new.map(|v| v.to_vec()),
});
}
}
#[test]
fn test_hook_on_put() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
let key = 1u64.to_be_bytes();
let val1 = 100u64.to_be_bytes();
let val2 = 200u64.to_be_bytes();
tree.put(&key, &val1).unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, key);
assert_eq!(evs[0].old, None);
assert_eq!(evs[0].new, Some(val1.to_vec()));
tree.put(&key, &val2).unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, key);
assert_eq!(evs[0].old, Some(val1.to_vec()));
assert_eq!(evs[0].new, Some(val2.to_vec()));
}
#[test]
fn test_hook_on_delete() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
let key = 1u64.to_be_bytes();
let val = 100u64.to_be_bytes();
tree.put(&key, &val).unwrap();
events.lock().clear();
tree.delete(&key).unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, key);
assert_eq!(evs[0].old, Some(val.to_vec()));
assert_eq!(evs[0].new, None);
}
#[test]
fn test_hook_on_insert() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
let key = 1u64.to_be_bytes();
let val = 100u64.to_be_bytes();
tree.insert(&key, &val).unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, key);
assert_eq!(evs[0].old, None);
assert_eq!(evs[0].new, Some(val.to_vec()));
}
#[test]
fn test_hook_on_cas() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
let key = 1u64.to_be_bytes();
let val1 = 100u64.to_be_bytes();
let val2 = 200u64.to_be_bytes();
tree.put(&key, &val1).unwrap();
events.lock().clear();
tree.cas(&key, &val1, &val2).unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, key);
assert_eq!(evs[0].old, Some(val1.to_vec()));
assert_eq!(evs[0].new, Some(val2.to_vec()));
}
#[test]
fn test_hook_not_called_on_compaction() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let mut config = Config::test();
config.max_file_size = 4096;
config.compaction_threshold = 0.1;
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
config,
TrackingHook::new(events.clone()),
)
.unwrap();
for i in 0..100u64 {
let key = i.to_be_bytes();
let val = i.to_be_bytes();
tree.put(&key, &val).unwrap();
}
for i in 0..50u64 {
let key = i.to_be_bytes();
let val = (i + 1000).to_be_bytes();
tree.put(&key, &val).unwrap();
}
events.lock().clear();
tree.compact().unwrap();
let evs = take_events(&events);
assert!(
evs.is_empty(),
"hook should not be called during compaction, but got {} events",
evs.len()
);
for i in 0..100u64 {
let key = i.to_be_bytes();
let expected = if i < 50 {
(i + 1000).to_be_bytes()
} else {
i.to_be_bytes()
};
assert_eq!(tree.get(&key).unwrap(), expected);
}
}
#[cfg(feature = "var-collections")]
#[test]
fn test_hook_needs_old_value_false_var_tree() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = VarTree::<[u8; 8], NoOldHook>::open_hooked(
dir.path(),
Config::test(),
NoOldHook::new(events.clone()),
)
.unwrap();
let key = 1u64.to_be_bytes();
tree.put(&key, b"hello").unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].old, None);
tree.put(&key, b"world").unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].old, None);
assert_eq!(evs[0].new, Some(b"world".to_vec()));
}
#[cfg(feature = "var-collections")]
#[test]
fn test_hook_var_tree() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = VarTree::<[u8; 8], TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
let key = 1u64.to_be_bytes();
tree.put(&key, b"hello").unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].old, None);
assert_eq!(evs[0].new, Some(b"hello".to_vec()));
tree.put(&key, b"world").unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].old, Some(b"hello".to_vec()));
assert_eq!(evs[0].new, Some(b"world".to_vec()));
tree.delete(&key).unwrap();
let evs = take_events(&events);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].old, Some(b"world".to_vec()));
assert_eq!(evs[0].new, None);
}
#[test]
fn test_hook_delete_nonexistent() {
let dir = tempdir().unwrap();
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
let key = 99u64.to_be_bytes();
let result = tree.delete(&key).unwrap();
assert!(result.is_none());
let evs = take_events(&events);
assert!(
evs.is_empty(),
"hook should not fire on delete of missing key"
);
}
type InitEvents = Arc<Mutex<HashMap<[u8; 8], Vec<u8>>>>;
struct InitOnlyHook {
inits: InitEvents,
}
impl InitOnlyHook {
fn new(inits: InitEvents) -> Self {
Self { inits }
}
}
impl WriteHook<[u8; 8]> for InitOnlyHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _key: &[u8; 8], _old: Option<&[u8]>, _new: Option<&[u8]>) {
}
fn on_init(&self, key: &[u8; 8], value: &[u8]) {
self.inits.lock().insert(*key, value.to_vec());
}
}
struct InitWriteHook {
inits: InitEvents,
writes: Events,
}
impl InitWriteHook {
fn new(inits: InitEvents, writes: Events) -> Self {
Self { inits, writes }
}
}
impl WriteHook<[u8; 8]> for InitWriteHook {
const NEEDS_OLD_VALUE: bool = true;
const NEEDS_INIT: bool = true;
fn on_write(&self, key: &[u8; 8], old: Option<&[u8]>, new: Option<&[u8]>) {
self.writes.lock().push(HookEvent {
key: *key,
old: old.map(|v| v.to_vec()),
new: new.map(|v| v.to_vec()),
});
}
fn on_init(&self, key: &[u8; 8], value: &[u8]) {
self.inits.lock().insert(*key, value.to_vec());
}
}
#[test]
fn test_on_init_fires_via_migrate() {
let dir = tempdir().unwrap();
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let tree = ConstTree::<[u8; 8], 8, InitOnlyHook>::open_hooked(
dir.path(),
Config::test(),
InitOnlyHook::new(inits.clone()),
)
.unwrap();
for i in 0..10u64 {
tree.put(&i.to_be_bytes(), &(i * 100).to_be_bytes())
.unwrap();
}
let mutated = tree.migrate(|_, _| MigrateAction::Keep).unwrap();
assert_eq!(mutated, 0);
let inits = inits.lock();
assert_eq!(inits.len(), 10);
for i in 0..10u64 {
let key = i.to_be_bytes();
assert_eq!(inits[&key], (i * 100).to_be_bytes().to_vec());
}
}
#[test]
fn test_on_init_after_reopen() {
let dir = tempdir().unwrap();
{
let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), Config::test()).unwrap();
for i in 0..20u64 {
tree.put(&i.to_be_bytes(), &(i * 10).to_be_bytes()).unwrap();
}
tree.close().unwrap();
}
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let tree = ConstTree::<[u8; 8], 8, InitOnlyHook>::open_hooked(
dir.path(),
Config::test(),
InitOnlyHook::new(inits.clone()),
)
.unwrap();
tree.migrate(|_, _| MigrateAction::Keep).unwrap();
let inits = inits.lock();
assert_eq!(inits.len(), 20);
for i in 0..20u64 {
let key = i.to_be_bytes();
assert_eq!(inits[&key], (i * 10).to_be_bytes().to_vec());
}
}
#[test]
fn test_on_init_skips_deleted() {
let dir = tempdir().unwrap();
{
let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), Config::test()).unwrap();
for i in 0..10u64 {
tree.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
for i in (0..10u64).step_by(2) {
tree.delete(&i.to_be_bytes()).unwrap();
}
tree.close().unwrap();
}
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let tree = ConstTree::<[u8; 8], 8, InitOnlyHook>::open_hooked(
dir.path(),
Config::test(),
InitOnlyHook::new(inits.clone()),
)
.unwrap();
tree.migrate(|_, _| MigrateAction::Keep).unwrap();
let inits = inits.lock();
assert_eq!(inits.len(), 5, "only odd keys should survive");
for i in (1..10u64).step_by(2) {
assert!(inits.contains_key(&i.to_be_bytes()));
}
}
#[test]
fn test_on_write_not_fired_during_migrate() {
let dir = tempdir().unwrap();
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let writes: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, InitWriteHook>::open_hooked(
dir.path(),
Config::test(),
InitWriteHook::new(inits.clone(), writes.clone()),
)
.unwrap();
for i in 0..5u64 {
tree.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
writes.lock().clear();
tree.migrate(|key, _val| {
let new_val = [key[7] + 1, 0, 0, 0, 0, 0, 0, 0];
MigrateAction::Update(new_val)
})
.unwrap();
let write_evs = take_events(&writes);
assert!(
write_evs.is_empty(),
"on_write should not fire during migrate, got {} events",
write_evs.len()
);
let inits = inits.lock();
assert_eq!(
inits.len(),
5,
"on_init should fire for all updated entries"
);
}
#[test]
fn test_on_init_receives_updated_value() {
let dir = tempdir().unwrap();
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let tree = ConstTree::<[u8; 8], 8, InitOnlyHook>::open_hooked(
dir.path(),
Config::test(),
InitOnlyHook::new(inits.clone()),
)
.unwrap();
for i in 0..5u64 {
tree.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
tree.migrate(|_key, val| {
let v = u64::from_be_bytes(*val);
MigrateAction::Update((v * 2).to_be_bytes())
})
.unwrap();
let inits = inits.lock();
for i in 0..5u64 {
let key = i.to_be_bytes();
assert_eq!(
inits[&key],
(i * 2).to_be_bytes().to_vec(),
"on_init should receive the updated (new) value"
);
}
}
#[test]
fn test_on_init_not_fired_on_migrate_delete() {
let dir = tempdir().unwrap();
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let tree = ConstTree::<[u8; 8], 8, InitOnlyHook>::open_hooked(
dir.path(),
Config::test(),
InitOnlyHook::new(inits.clone()),
)
.unwrap();
for i in 0..10u64 {
tree.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
tree.migrate(|key, _| {
let v = u64::from_be_bytes(*key);
if v % 2 == 0 {
MigrateAction::Delete
} else {
MigrateAction::Keep
}
})
.unwrap();
let inits = inits.lock();
assert_eq!(inits.len(), 5, "on_init should only fire for kept entries");
for i in (1..10u64).step_by(2) {
assert!(inits.contains_key(&i.to_be_bytes()));
}
}
#[test]
fn test_on_init_plus_on_write() {
let dir = tempdir().unwrap();
{
let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), Config::test()).unwrap();
for i in 0..5u64 {
tree.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
tree.close().unwrap();
}
let inits: InitEvents = Arc::new(Mutex::new(HashMap::new()));
let writes: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, InitWriteHook>::open_hooked(
dir.path(),
Config::test(),
InitWriteHook::new(inits.clone(), writes.clone()),
)
.unwrap();
tree.migrate(|_, _| MigrateAction::Keep).unwrap();
assert_eq!(inits.lock().len(), 5, "on_init should fire for 5 entries");
assert!(
writes.lock().is_empty(),
"on_write should not fire during migrate"
);
let key = 0u64.to_be_bytes();
let new_val = 999u64.to_be_bytes();
tree.put(&key, &new_val).unwrap();
let write_evs = take_events(&writes);
assert_eq!(write_evs.len(), 1);
assert_eq!(write_evs[0].key, key);
assert_eq!(write_evs[0].old, Some(0u64.to_be_bytes().to_vec()));
assert_eq!(write_evs[0].new, Some(new_val.to_vec()));
}
#[test]
fn test_on_init_not_fired_when_needs_init_false() {
let dir = tempdir().unwrap();
{
let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), Config::test()).unwrap();
for i in 0..5u64 {
tree.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
tree.close().unwrap();
}
let events: Events = Arc::new(Mutex::new(Vec::new()));
let tree = ConstTree::<[u8; 8], 8, TrackingHook>::open_hooked(
dir.path(),
Config::test(),
TrackingHook::new(events.clone()),
)
.unwrap();
tree.migrate(|_, _| MigrateAction::Keep).unwrap();
let evs = take_events(&events);
assert!(
evs.is_empty(),
"on_write should not fire during migrate, and on_init is disabled"
);
}