#![cfg(all(
feature = "typed-tree",
feature = "armour",
feature = "rpc",
feature = "rapira-codec"
))]
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use armdb::armour::{Db, RpcHandler};
use armdb::{CollectionMeta, Config, DbError, FixedConfig, NoHook, RapiraCodec};
use armour_core::GetType;
use armour_rpc::protocol::UpsertKey;
use rapira::Rapira;
use tempfile::tempdir;
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct TestItem {
value: u64,
}
impl CollectionMeta for TestItem {
type SelfId = [u8; 8];
const NAME: &'static str = "integ_test_items";
const VERSION: u16 = 1;
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
)]
#[repr(C)]
struct ZeroVal(u64);
impl CollectionMeta for ZeroVal {
type SelfId = [u8; 8];
const NAME: &'static str = "integ_zero_items";
const VERSION: u16 = 1;
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
)]
#[repr(C)]
struct FixedVal(u64);
impl CollectionMeta for FixedVal {
type SelfId = [u8; 8];
const NAME: &'static str = "integ_fixed_items";
const VERSION: u16 = 1;
}
fn k(id: u64) -> [u8; 8] {
id.to_be_bytes()
}
fn k_bytes(id: u64) -> Vec<u8> {
k(id).to_vec()
}
fn encode_item(item: &TestItem) -> Vec<u8> {
let codec = RapiraCodec;
let mut buf = Vec::new();
armdb::Codec::encode_to(&codec, item, &mut buf).unwrap();
buf
}
fn get_handler(db: &Db, name: &str) -> Arc<dyn RpcHandler> {
let hash = xxhash_rust::xxh3::xxh3_64(name.as_bytes());
let tree_map = db.build_tree_map();
tree_map
.get(&hash)
.cloned()
.unwrap_or_else(|| panic!("handler for '{name}' not registered"))
}
#[test]
fn rpc_error_codes_are_semantic() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
let val = encode_item(&TestItem { value: 42 });
let err = h
.upsert(UpsertKey::Provided(k_bytes(1)), Some(true), val.clone())
.expect_err("expected KeyNotFound");
assert_eq!(err.status_code(), 404, "upsert update-only absent → 404");
h.upsert(UpsertKey::Provided(k_bytes(1)), None, val.clone())
.expect("upsert unconditional");
let err = h
.upsert(UpsertKey::Provided(k_bytes(1)), Some(false), val.clone())
.expect_err("expected KeyExists");
assert_eq!(err.status_code(), 409, "upsert insert-only existing → 409");
let err = h
.remove(&k_bytes(1), true)
.expect_err("expected NotImplemented");
assert_eq!(err.status_code(), 501, "remove soft → 501");
let err = h
.take(&k_bytes(1), true)
.expect_err("expected NotImplemented");
assert_eq!(err.status_code(), 501, "take soft → 501");
let short_key = vec![0u8; 4]; let err = h.contains(&short_key).expect_err("expected KeyNotFound");
assert_eq!(err.status_code(), 404, "wrong-length key → 404");
h.remove(&k_bytes(999), false)
.expect("remove absent must be idempotent (Ok)");
}
#[test]
fn take_returns_value_and_removes_key() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
let val = encode_item(&TestItem { value: 99 });
h.upsert(UpsertKey::Provided(k_bytes(10)), None, val.clone())
.expect("insert");
let taken = h.take(&k_bytes(10), false).expect("take");
assert!(taken.is_some(), "take should return the value bytes");
let present = h.contains(&k_bytes(10)).expect("contains");
assert!(!present, "key must be absent after take");
let cnt = h.count(false).expect("count");
assert_eq!(cnt, 0, "count must be 0 after take");
}
#[test]
fn take_soft_returns_501() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
let err = h
.take(&k_bytes(1), true)
.expect_err("expected NotImplemented");
assert_eq!(err.status_code(), 501, "take soft=true → 501");
}
#[test]
fn apply_batch_rejects_bad_key_length_whole_batch() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_zero_tree::<ZeroVal, 8, _>(Config::test(), NoHook, &[])
.expect("open zero tree");
let h = get_handler(&db, ZeroVal::NAME);
let valid_val: Vec<u8> = vec![0u8; 8];
let batch: Vec<(Vec<u8>, Option<Vec<u8>>)> = vec![
(k_bytes(1), Some(valid_val.clone())),
(vec![0u8; 4], Some(valid_val.clone())), (k_bytes(3), Some(valid_val.clone())),
];
let result = h.apply_batch(batch);
assert!(
result.is_err(),
"batch with invalid key length must be rejected"
);
match result.unwrap_err() {
DbError::Client(_) => {}
other => panic!("expected Client error, got: {other:?}"),
}
assert_eq!(h.count(true).expect("count"), 0, "no partial writes");
}
#[test]
fn apply_batch_rejects_bad_value_length_whole_batch() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_zero_tree::<ZeroVal, 8, _>(Config::test(), NoHook, &[])
.expect("open zero tree");
let h = get_handler(&db, ZeroVal::NAME);
let valid_val: Vec<u8> = vec![0u8; 8];
let bad_val: Vec<u8> = vec![0u8; 5]; let batch: Vec<(Vec<u8>, Option<Vec<u8>>)> = vec![
(k_bytes(1), Some(valid_val.clone())),
(k_bytes(2), Some(bad_val)),
(k_bytes(3), Some(valid_val.clone())),
];
let result = h.apply_batch(batch);
assert!(
result.is_err(),
"batch with invalid value length must be rejected"
);
match result.unwrap_err() {
DbError::Client(_) => {}
other => panic!("expected Client error, got: {other:?}"),
}
assert_eq!(h.count(true).expect("count"), 0, "no partial writes");
}
#[test]
fn upsert_insert_only_concurrent_at_most_one_wins() {
let dir = tempdir().expect("tmp");
let db = Arc::new(Db::open(dir.path()).expect("open"));
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let ok_count = Arc::new(AtomicUsize::new(0));
let conflict_count = Arc::new(AtomicUsize::new(0));
let n_threads = 8;
let val = encode_item(&TestItem { value: 1 });
let handles: Vec<_> = (0..n_threads)
.map(|_| {
let db = Arc::clone(&db);
let val = val.clone();
let ok_count = Arc::clone(&ok_count);
let conflict_count = Arc::clone(&conflict_count);
std::thread::spawn(move || {
let h = get_handler(&db, TestItem::NAME);
let result = h.upsert(UpsertKey::Provided(k_bytes(42)), Some(false), val.clone());
match result {
Ok(_) => {
ok_count.fetch_add(1, Ordering::Relaxed);
}
Err(DbError::KeyExists) => {
conflict_count.fetch_add(1, Ordering::Relaxed);
}
Err(e) => panic!("unexpected error: {e:?}"),
}
})
})
.collect();
for h in handles {
h.join().expect("thread panicked");
}
let oks = ok_count.load(Ordering::Relaxed);
let conflicts = conflict_count.load(Ordering::Relaxed);
assert_eq!(oks, 1, "exactly one insert must succeed; got {oks}");
assert_eq!(
conflicts,
n_threads - 1,
"remaining {} must get KeyExists",
n_threads - 1
);
}
#[test]
fn upsert_update_only_missing_key_returns_404() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
let val = encode_item(&TestItem { value: 7 });
let err = h
.upsert(UpsertKey::Provided(k_bytes(99)), Some(true), val)
.expect_err("expected KeyNotFound");
assert_eq!(err.status_code(), 404, "update-only on missing key → 404");
}
#[test]
fn count_exact_matches_iter_count_for_tree() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
const N: u64 = 50;
for i in 0..N {
let val = encode_item(&TestItem { value: i });
h.upsert(UpsertKey::Provided(k_bytes(i)), None, val)
.expect("insert");
}
let approx = h.count(false).expect("count approx");
let exact = h.count(true).expect("count exact");
assert_eq!(approx, N, "count(false) must equal {N}");
assert_eq!(exact, N, "count(true) must equal {N}");
}
#[test]
fn contains_returns_correct_bool() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
let val = encode_item(&TestItem { value: 5 });
h.upsert(UpsertKey::Provided(k_bytes(5)), None, val)
.expect("insert");
let present = h.contains(&k_bytes(5)).expect("contains present");
assert!(present, "contains must be true for inserted key");
let absent = h.contains(&k_bytes(99)).expect("contains absent");
assert!(!absent, "contains must be false for missing key");
}
#[test]
fn entry_len_returns_size_for_typed_tree() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open tree");
let h = get_handler(&db, TestItem::NAME);
let item = TestItem { value: 42 };
let val = encode_item(&item);
let expected_len = val.len() as u32;
h.upsert(UpsertKey::Provided(k_bytes(1)), None, val)
.expect("insert");
let len = h.entry_len(&k_bytes(1)).expect("entry_len present");
assert_eq!(len, Some(expected_len), "entry_len must equal encoded size");
let absent = h.entry_len(&k_bytes(999)).expect("entry_len absent");
assert_eq!(absent, None, "entry_len must be None for absent key");
}
#[test]
fn entry_len_zero_tree_returns_v() {
let dir = tempdir().expect("tmp");
let db = Db::open(dir.path()).expect("open");
let _tree = db
.open_zero_tree::<ZeroVal, 8, _>(Config::test(), NoHook, &[])
.expect("open zero tree");
let h = get_handler(&db, ZeroVal::NAME);
let zero_val: Vec<u8> = vec![0xABu8; 8];
h.upsert(UpsertKey::Provided(k_bytes(7)), None, zero_val)
.expect("insert");
let len = h.entry_len(&k_bytes(7)).expect("entry_len");
assert_eq!(len, Some(8), "ZeroTree entry_len must be Some(V=8)");
let absent = h.entry_len(&k_bytes(999)).expect("entry_len absent");
assert_eq!(absent, None, "absent key → None");
}
#[test]
fn db_close_flushes_mixed_collection_types() {
let dir = tempdir().expect("tmp");
{
let db = Db::open(dir.path()).expect("open");
let typed = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("open typed tree");
let zero = db
.open_zero_tree::<ZeroVal, 8, _>(Config::test(), NoHook, &[])
.expect("open zero tree");
let fixed = db
.open_zero_tree_fixed::<FixedVal, 8, _>(FixedConfig::test(), NoHook, &[])
.expect("open fixed tree");
typed
.put(&k(1), TestItem { value: 111 })
.expect("put typed");
zero.put(&k(2), &ZeroVal(222)).expect("put zero");
fixed.put(&k(3), &FixedVal(333)).expect("put fixed");
db.close().expect("close");
}
{
let db = Db::open(dir.path()).expect("reopen");
let typed = db
.open_typed_tree::<TestItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("reopen typed tree");
let zero = db
.open_zero_tree::<ZeroVal, 8, _>(Config::test(), NoHook, &[])
.expect("reopen zero tree");
let fixed = db
.open_zero_tree_fixed::<FixedVal, 8, _>(FixedConfig::test(), NoHook, &[])
.expect("reopen fixed tree");
let t = typed.get(&k(1)).expect("typed key missing after reopen");
assert_eq!(t.value, 111, "typed value mismatch");
let z = zero.get(&k(2)).expect("zero key missing after reopen");
assert_eq!(z.0, 222, "zero value mismatch");
let f = fixed.get(&k(3)).expect("fixed key missing after reopen");
assert_eq!(f.0, 333, "fixed value mismatch");
}
}