#![cfg(feature = "git")]
mod common;
use common::setup_repo_and_dataset;
use prollytree::digest::ValueDigest;
use prollytree::git::versioned_store::FileNamespacedKvStore;
use prollytree::storage::NodeStorage;
const N: usize = 32;
#[test]
fn small_value_stays_inline_when_threshold_set() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(1024));
let payload = b"short value".to_vec();
{
let mut personal = store.namespace("personal");
personal.insert(b"small".to_vec(), payload.clone()).unwrap();
}
store.commit("small").unwrap();
let personal = store.namespace("personal");
assert_eq!(personal.get(b"small"), Some(payload.clone()));
let hash = ValueDigest::<N>::new(&payload);
assert!(
store.inner_storage().get_blob(&hash).is_none(),
"small value should not be in blob store"
);
}
#[test]
fn large_value_lands_in_blob_store_but_get_returns_original() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let payload: Vec<u8> = (0..2048).map(|i| (i % 251) as u8).collect();
{
let mut personal = store.namespace("personal");
personal.insert(b"big".to_vec(), payload.clone()).unwrap();
}
store.commit("big").unwrap();
let personal = store.namespace("personal");
assert_eq!(personal.get(b"big"), Some(payload.clone()));
let hash = ValueDigest::<N>::new(&payload);
let blob = store
.inner_storage()
.get_blob(&hash)
.expect("blob should be present after externalisation");
assert_eq!(blob, payload);
}
#[test]
fn boundary_at_threshold_stays_inline() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(100));
let payload: Vec<u8> = vec![0xAB; 100];
{
let mut personal = store.namespace("personal");
personal
.insert(b"borderline".to_vec(), payload.clone())
.unwrap();
}
store.commit("border").unwrap();
let hash = ValueDigest::<N>::new(&payload);
assert!(
store.inner_storage().get_blob(&hash).is_none(),
"value at threshold should stay inline"
);
let personal = store.namespace("personal");
assert_eq!(personal.get(b"borderline"), Some(payload));
}
#[test]
fn one_byte_above_threshold_externalises() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(100));
let payload: Vec<u8> = vec![0xCD; 101];
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), payload.clone()).unwrap();
}
store.commit("over").unwrap();
let hash = ValueDigest::<N>::new(&payload);
assert!(
store.inner_storage().get_blob(&hash).is_some(),
"one byte above threshold should externalise"
);
let personal = store.namespace("personal");
assert_eq!(personal.get(b"k"), Some(payload));
}
#[test]
fn externalised_value_survives_commit_and_reopen() {
let (_temp, dataset) = setup_repo_and_dataset();
let payload: Vec<u8> = (0..1_048_576).map(|i| (i % 251) as u8).collect(); let hash = ValueDigest::<N>::new(&payload);
{
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64 * 1024));
let mut personal = store.namespace("personal");
personal
.insert(b"document".to_vec(), payload.clone())
.unwrap();
drop(personal);
store.commit("ingest").unwrap();
}
let mut store = FileNamespacedKvStore::<N>::open(&dataset).unwrap();
let personal = store.namespace("personal");
let got = personal.get(b"document").expect("should be present");
assert_eq!(got.len(), payload.len());
assert_eq!(got, payload);
assert!(store.inner_storage().get_blob(&hash).is_some());
}
#[test]
fn threshold_disabled_keeps_old_behaviour() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
assert!(store.externalize_threshold().is_none());
let payload: Vec<u8> = vec![0x42; 5_000];
let hash = ValueDigest::<N>::new(&payload);
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), payload.clone()).unwrap();
}
store.commit("inline").unwrap();
assert!(store.inner_storage().get_blob(&hash).is_none());
let personal = store.namespace("personal");
assert_eq!(personal.get(b"k"), Some(payload));
}
#[test]
fn staged_large_value_is_visible_before_commit() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let payload: Vec<u8> = vec![0xCC; 200];
let mut personal = store.namespace("personal");
personal
.insert(b"staged".to_vec(), payload.clone())
.unwrap();
assert_eq!(personal.get(b"staged"), Some(payload));
}
#[test]
fn delete_then_get_returns_none_even_when_externalised() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let payload = vec![0xDD; 500];
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), payload).unwrap();
}
store.commit("write").unwrap();
{
let mut personal = store.namespace("personal");
assert!(personal.delete(b"k").unwrap());
}
store.commit("delete").unwrap();
let personal = store.namespace("personal");
assert!(personal.get(b"k").is_none());
}
#[test]
fn upsert_changes_externalised_value() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let v1 = vec![0xAA; 300];
let v2 = vec![0xBB; 400];
let h1 = ValueDigest::<N>::new(&v1);
let h2 = ValueDigest::<N>::new(&v2);
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), v1).unwrap();
}
store.commit("v1").unwrap();
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), v2.clone()).unwrap();
}
store.commit("v2").unwrap();
assert!(store.inner_storage().get_blob(&h1).is_some());
assert!(store.inner_storage().get_blob(&h2).is_some());
let personal = store.namespace("personal");
assert_eq!(personal.get(b"k"), Some(v2));
}
#[test]
fn gc_blobs_empty_store_is_noop() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
let report = store.gc_blobs().unwrap();
assert_eq!(report.total, 0);
assert_eq!(report.referenced, 0);
assert_eq!(report.removed, 0);
assert!(report.errors.is_empty());
}
#[test]
fn gc_blobs_keeps_referenced_blobs() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let p1: Vec<u8> = vec![0xAA; 200];
let p2: Vec<u8> = vec![0xBB; 300];
let h1 = ValueDigest::<N>::new(&p1);
let h2 = ValueDigest::<N>::new(&p2);
{
let mut personal = store.namespace("personal");
personal.insert(b"a".to_vec(), p1).unwrap();
personal.insert(b"b".to_vec(), p2).unwrap();
}
store.commit("two").unwrap();
let report = store.gc_blobs().unwrap();
assert_eq!(report.total, 2);
assert_eq!(report.referenced, 2);
assert_eq!(report.removed, 0);
assert!(store.inner_storage().get_blob(&h1).is_some());
assert!(store.inner_storage().get_blob(&h2).is_some());
}
#[test]
fn gc_blobs_removes_orphans_from_upsert() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let v1 = vec![0xAA; 200];
let v2 = vec![0xBB; 300];
let h1 = ValueDigest::<N>::new(&v1);
let h2 = ValueDigest::<N>::new(&v2);
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), v1).unwrap();
}
store.commit("v1").unwrap();
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), v2.clone()).unwrap();
}
store.commit("v2").unwrap();
assert!(store.inner_storage().get_blob(&h1).is_some());
assert!(store.inner_storage().get_blob(&h2).is_some());
let report = store.gc_blobs().unwrap();
assert_eq!(report.total, 2);
assert_eq!(report.referenced, 1);
assert_eq!(report.removed, 1);
assert_eq!(report.remaining(), 1);
assert!(store.inner_storage().get_blob(&h1).is_none());
assert!(store.inner_storage().get_blob(&h2).is_some());
let personal = store.namespace("personal");
assert_eq!(personal.get(b"k"), Some(v2));
}
#[test]
fn gc_blobs_removes_orphans_from_delete() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let payload = vec![0xCC; 500];
let hash = ValueDigest::<N>::new(&payload);
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), payload).unwrap();
}
store.commit("insert").unwrap();
assert!(store.inner_storage().get_blob(&hash).is_some());
{
let mut personal = store.namespace("personal");
assert!(personal.delete(b"k").unwrap());
}
store.commit("delete").unwrap();
assert!(store.inner_storage().get_blob(&hash).is_some());
let report = store.gc_blobs().unwrap();
assert_eq!(report.total, 1);
assert_eq!(report.referenced, 0);
assert_eq!(report.removed, 1);
assert!(store.inner_storage().get_blob(&hash).is_none());
}
#[test]
fn gc_blobs_keeps_blobs_across_namespaces() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let pa = vec![0xAA; 200];
let pb = vec![0xBB; 300];
let ha = ValueDigest::<N>::new(&pa);
let hb = ValueDigest::<N>::new(&pb);
{
let mut a = store.namespace("ns_a");
a.insert(b"x".to_vec(), pa).unwrap();
}
{
let mut b = store.namespace("ns_b");
b.insert(b"y".to_vec(), pb).unwrap();
}
store.commit("both").unwrap();
drop(store);
let mut store = FileNamespacedKvStore::<N>::open(&dataset).unwrap();
let report = store.gc_blobs().unwrap();
assert_eq!(report.total, 2);
assert_eq!(
report.referenced, 2,
"gc must load all namespaces from registry"
);
assert_eq!(report.removed, 0);
assert!(store.inner_storage().get_blob(&ha).is_some());
assert!(store.inner_storage().get_blob(&hb).is_some());
}
#[test]
fn gc_blobs_idempotent() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let v1 = vec![0xAA; 200];
let v2 = vec![0xBB; 300];
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), v1).unwrap();
}
store.commit("v1").unwrap();
{
let mut personal = store.namespace("personal");
personal.insert(b"k".to_vec(), v2).unwrap();
}
store.commit("v2").unwrap();
let first = store.gc_blobs().unwrap();
assert_eq!(first.removed, 1);
let second = store.gc_blobs().unwrap();
assert_eq!(second.total, 1);
assert_eq!(second.referenced, 1);
assert_eq!(second.removed, 0);
}
#[test]
fn same_large_value_under_two_keys_writes_blob_once() {
let (_temp, dataset) = setup_repo_and_dataset();
let mut store = FileNamespacedKvStore::<N>::init(&dataset).unwrap();
store.set_externalize_threshold(Some(64));
let payload: Vec<u8> = (0..500).map(|i| i as u8).collect();
let hash = ValueDigest::<N>::new(&payload);
{
let mut personal = store.namespace("personal");
personal.insert(b"a".to_vec(), payload.clone()).unwrap();
personal.insert(b"b".to_vec(), payload.clone()).unwrap();
}
store.commit("dedup").unwrap();
let blob = store.inner_storage().get_blob(&hash).expect("blob exists");
assert_eq!(blob, payload);
let personal = store.namespace("personal");
assert_eq!(personal.get(b"a"), Some(payload.clone()));
assert_eq!(personal.get(b"b"), Some(payload));
}