use hashtree_cli::storage::{HashtreeStore, PRIORITY_FOLLOWED, PRIORITY_OTHER, PRIORITY_OWN};
use hashtree_config::StorageBackend;
use hashtree_core::from_hex;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn test_store(max_size_bytes: u64) -> (HashtreeStore, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let store = HashtreeStore::with_options_and_backend(
temp_dir.path(),
None,
max_size_bytes,
true,
&StorageBackend::Fs,
)
.expect("Failed to create store");
(store, temp_dir)
}
fn add_blob(store: &HashtreeStore, data: &[u8]) -> [u8; 32] {
let hash_hex = store.put_blob(data).expect("Failed to put blob");
from_hex(&hash_hex).expect("Invalid hash")
}
fn used_bytes(store: &HashtreeStore) -> u64 {
store
.router()
.stats()
.expect("Failed to read stats")
.total_bytes
}
#[test]
fn test_tree_indexing() {
let (store, _tmp) = test_store(1024 * 1024 * 1024);
let data = b"Hello, world!";
let hash = add_blob(&store, data);
store
.index_tree(
&hash,
"test_owner",
Some("test_tree"),
PRIORITY_FOLLOWED,
None,
)
.expect("Failed to index tree");
let meta = store.get_tree_meta(&hash).expect("Failed to get meta");
assert!(meta.is_some(), "Tree should be indexed");
let meta = meta.unwrap();
assert_eq!(meta.owner, "test_owner");
assert_eq!(meta.name, Some("test_tree".to_string()));
assert_eq!(meta.priority, PRIORITY_FOLLOWED);
assert!(meta.total_size > 0, "Should have tracked size");
}
#[test]
fn test_list_indexed_trees() {
let (store, _tmp) = test_store(1024 * 1024 * 1024);
let data1 = b"Tree 1 content";
let hash1 = add_blob(&store, data1);
store
.index_tree(&hash1, "owner1", Some("tree1"), PRIORITY_OWN, None)
.expect("Failed to index tree 1");
thread::sleep(Duration::from_millis(10));
let data2 = b"Tree 2 content";
let hash2 = add_blob(&store, data2);
store
.index_tree(&hash2, "owner2", Some("tree2"), PRIORITY_FOLLOWED, None)
.expect("Failed to index tree 2");
let trees = store.list_indexed_trees().expect("Failed to list trees");
assert_eq!(trees.len(), 2, "Should have 2 indexed trees");
let hashes: Vec<[u8; 32]> = trees.iter().map(|(h, _)| *h).collect();
assert!(hashes.contains(&hash1));
assert!(hashes.contains(&hash2));
}
#[test]
fn test_tracked_size() {
let (store, _tmp) = test_store(1024 * 1024 * 1024);
let initial = store.tracked_size().expect("Failed to get tracked size");
assert_eq!(initial, 0, "Initial tracked size should be 0");
let data = vec![0u8; 1000]; let hash = add_blob(&store, &data);
store
.index_tree(&hash, "owner", None, PRIORITY_OTHER, None)
.expect("Failed to index tree");
let tracked = store.tracked_size().expect("Failed to get tracked size");
assert_eq!(tracked, 1000, "Tracked size should be 1000 bytes");
}
#[test]
fn test_storage_by_priority() {
let (store, _tmp) = test_store(1024 * 1024 * 1024);
let data_own = vec![0u8; 500];
let hash_own = add_blob(&store, &data_own);
store
.index_tree(&hash_own, "me", Some("own"), PRIORITY_OWN, None)
.unwrap();
let data_followed = vec![1u8; 300];
let hash_followed = add_blob(&store, &data_followed);
store
.index_tree(
&hash_followed,
"friend",
Some("followed"),
PRIORITY_FOLLOWED,
None,
)
.unwrap();
let data_other = vec![2u8; 200];
let hash_other = add_blob(&store, &data_other);
store
.index_tree(&hash_other, "random", Some("other"), PRIORITY_OTHER, None)
.unwrap();
let by_priority = store
.storage_by_priority()
.expect("Failed to get by priority");
assert_eq!(by_priority.own, 500, "Own should be 500 bytes");
assert_eq!(by_priority.followed, 300, "Followed should be 300 bytes");
assert_eq!(by_priority.other, 200, "Other should be 200 bytes");
}
#[test]
fn test_eviction_under_limit() {
let (store, _tmp) = test_store(10000);
let data = vec![0u8; 100];
let hash = add_blob(&store, &data);
store
.index_tree(&hash, "owner", None, PRIORITY_OTHER, None)
.unwrap();
let freed = store.evict_if_needed().expect("Eviction failed");
assert_eq!(freed, 0, "Should not evict when under limit");
assert!(store.blob_exists(&hash).unwrap(), "Blob should still exist");
}
#[test]
fn durable_blob_write_rejects_when_owned_blobs_fill_limit() {
let (store, _tmp) = test_store(500);
let first = vec![1u8; 300];
let owner = [7u8; 32];
let first_hash = from_hex(
&store
.put_owned_blob(&first, &owner)
.expect("first owned blob"),
)
.expect("first hash");
let second = vec![2u8; 300];
let second_hash = hashtree_core::sha256(&second);
let error = store
.put_owned_blob(&second, &owner)
.expect_err("owned durable data should not be displaced by a new durable blob");
assert!(
error.to_string().contains("storage limit"),
"unexpected error: {error}"
);
assert!(store.blob_exists(&first_hash).expect("first remains"));
assert!(!store.blob_exists(&second_hash).expect("second rejected"));
assert!(used_bytes(&store) <= 500);
}
#[test]
fn test_eviction_over_limit() {
let (store, _tmp) = test_store(500);
let data1 = vec![1u8; 200];
let hash1 = add_blob(&store, &data1);
store
.index_tree(&hash1, "owner1", Some("tree1"), PRIORITY_OTHER, None)
.unwrap();
thread::sleep(Duration::from_millis(10));
let data2 = vec![2u8; 200];
let hash2 = add_blob(&store, &data2);
store
.index_tree(&hash2, "owner2", Some("tree2"), PRIORITY_OTHER, None)
.unwrap();
thread::sleep(Duration::from_millis(10));
let data3 = vec![3u8; 200];
let hash3 = add_blob(&store, &data3);
store
.index_tree(&hash3, "owner3", Some("tree3"), PRIORITY_OTHER, None)
.unwrap();
let freed = store.evict_if_needed().expect("Eviction failed");
assert!(freed > 0, "Should have evicted something");
let meta1 = store.get_tree_meta(&hash1).unwrap();
assert!(meta1.is_none(), "Oldest tree should be evicted");
let meta2 = store.get_tree_meta(&hash2).unwrap();
let meta3 = store.get_tree_meta(&hash3).unwrap();
assert!(
meta2.is_some() || meta3.is_some(),
"Newer trees should remain"
);
}
#[test]
fn test_pinned_tree_protection() {
let (store, _tmp) = test_store(300);
let data_pinned = vec![0u8; 200];
let hash_pinned = add_blob(&store, &data_pinned);
store
.index_tree(&hash_pinned, "me", Some("pinned"), PRIORITY_OWN, None)
.unwrap();
store.pin(&hash_pinned).expect("Failed to pin");
let data_other = vec![1u8; 200];
let hash_other = add_blob(&store, &data_other);
store
.index_tree(&hash_other, "random", Some("other"), PRIORITY_OTHER, None)
.unwrap();
let freed = store.evict_if_needed().expect("Eviction failed");
let meta_pinned = store.get_tree_meta(&hash_pinned).unwrap();
assert!(
meta_pinned.is_some(),
"Pinned tree should be protected from eviction"
);
let meta_other = store.get_tree_meta(&hash_other).unwrap();
assert!(meta_other.is_none(), "Other tree should be evicted");
assert!(freed > 0, "Should have freed space by evicting other tree");
}
#[test]
fn test_own_tree_can_be_evicted() {
let (store, _tmp) = test_store(300);
let data_own = vec![0u8; 200];
let hash_own = add_blob(&store, &data_own);
store
.index_tree(&hash_own, "me", Some("own"), PRIORITY_OWN, None)
.unwrap();
let data_own2 = vec![1u8; 200];
let hash_own2 = add_blob(&store, &data_own2);
store
.index_tree(&hash_own2, "me", Some("own2"), PRIORITY_OWN, None)
.unwrap();
let freed = store.evict_if_needed().expect("Eviction failed");
assert!(
freed > 0,
"Should have evicted own tree when no other option"
);
}
#[test]
fn test_priority_order_eviction() {
let (store, _tmp) = test_store(400);
let data_other = vec![0u8; 150];
let hash_other = add_blob(&store, &data_other);
store
.index_tree(&hash_other, "random", Some("other"), PRIORITY_OTHER, None)
.unwrap();
thread::sleep(Duration::from_millis(10));
let data_followed = vec![1u8; 150];
let hash_followed = add_blob(&store, &data_followed);
store
.index_tree(
&hash_followed,
"friend",
Some("followed"),
PRIORITY_FOLLOWED,
None,
)
.unwrap();
thread::sleep(Duration::from_millis(10));
let data_own = vec![2u8; 150];
let hash_own = add_blob(&store, &data_own);
store
.index_tree(&hash_own, "me", Some("own"), PRIORITY_OWN, None)
.unwrap();
store.pin(&hash_own).expect("Failed to pin");
let freed = store.evict_if_needed().expect("Eviction failed");
let meta_other = store.get_tree_meta(&hash_other).unwrap();
assert!(
meta_other.is_none(),
"Other tree (lowest priority) should be evicted first"
);
let _meta_followed = store.get_tree_meta(&hash_followed).unwrap();
let meta_own = store.get_tree_meta(&hash_own).unwrap();
assert!(meta_own.is_some(), "Pinned tree should never be evicted");
assert!(freed > 0, "Should have freed space");
}
#[test]
fn test_unindex_tree() {
let (store, _tmp) = test_store(1024 * 1024 * 1024);
let data = vec![0u8; 500];
let hash = add_blob(&store, &data);
store
.index_tree(&hash, "owner", Some("test"), PRIORITY_OTHER, None)
.unwrap();
assert!(store.get_tree_meta(&hash).unwrap().is_some());
assert!(store.blob_exists(&hash).unwrap());
let freed = store.unindex_tree(&hash).expect("Unindex failed");
assert!(freed > 0, "Should have freed bytes");
assert!(store.get_tree_meta(&hash).unwrap().is_none());
assert!(
!store.blob_exists(&hash).unwrap(),
"Orphaned blob should be deleted"
);
}
#[test]
fn test_max_size_bytes_accessor() {
let (store, _tmp) = test_store(12345678);
assert_eq!(
store.max_size_bytes(),
12345678,
"max_size_bytes should return configured value"
);
}
#[test]
fn test_orphan_eviction_first() {
let (store, _tmp) = test_store(500);
let orphan_data = vec![0u8; 200];
let orphan_hash = add_blob(&store, &orphan_data);
let tree_data = vec![1u8; 200];
let tree_hash = add_blob(&store, &tree_data);
store
.index_tree(&tree_hash, "owner", Some("tree"), PRIORITY_OTHER, None)
.unwrap();
let extra_data = vec![2u8; 200];
let _extra_hash = add_blob(&store, &extra_data);
let freed = store.evict_if_needed().expect("Eviction failed");
assert!(freed > 0, "Should have freed space");
assert!(
!store.blob_exists(&orphan_hash).unwrap(),
"Orphan blob should be evicted first"
);
let meta = store.get_tree_meta(&tree_hash).unwrap();
assert!(
meta.is_some(),
"Indexed tree should remain after orphan eviction"
);
}
#[test]
fn test_pinned_not_evicted_as_orphan() {
let (store, _tmp) = test_store(300);
let pinned_data = vec![0u8; 200];
let pinned_hash = add_blob(&store, &pinned_data);
store.pin(&pinned_hash).expect("Failed to pin");
let extra_data = vec![1u8; 200];
let _extra_hash = add_blob(&store, &extra_data);
let _ = store.evict_if_needed();
assert!(
store.blob_exists(&pinned_hash).unwrap(),
"Pinned blob should not be evicted"
);
}
#[test]
fn test_ref_key_replaces_old_version() {
let (store, _tmp) = test_store(1024 * 1024 * 1024);
let data1 = vec![0u8; 100];
let hash1 = add_blob(&store, &data1);
store
.index_tree(
&hash1,
"owner",
Some("test"),
PRIORITY_OWN,
Some("owner/test"),
)
.unwrap();
assert!(store.get_tree_meta(&hash1).unwrap().is_some());
let data2 = vec![1u8; 100];
let hash2 = add_blob(&store, &data2);
store
.index_tree(
&hash2,
"owner",
Some("test"),
PRIORITY_OWN,
Some("owner/test"),
)
.unwrap();
assert!(
store.get_tree_meta(&hash1).unwrap().is_none(),
"Old version should be unindexed"
);
assert!(
store.get_tree_meta(&hash2).unwrap().is_some(),
"New version should be indexed"
);
}
#[test]
fn test_put_cached_blob_evicts_disposable_orphans() {
let (store, _tmp) = test_store(250);
let protected_hash = add_blob(&store, &vec![9u8; 100]);
store
.index_tree(
&protected_hash,
"owner",
Some("protected"),
PRIORITY_OTHER,
None,
)
.unwrap();
let disposable_hash = add_blob(&store, &vec![1u8; 100]);
assert!(store.blob_exists(&disposable_hash).unwrap());
let cached_hash = from_hex(&store.put_cached_blob(&vec![2u8; 100]).unwrap()).unwrap();
assert!(
store.blob_exists(&protected_hash).unwrap(),
"Indexed tree blob should be protected from cache eviction"
);
assert!(
!store.blob_exists(&disposable_hash).unwrap(),
"Disposable orphan should be evicted to make room"
);
assert!(
store.blob_exists(&cached_hash).unwrap(),
"New cached blob should be stored"
);
assert!(
used_bytes(&store) <= store.max_size_bytes(),
"Cache write should keep usage within limit"
);
}
#[test]
fn test_put_cached_blob_preserves_owned_blossom_blob() {
let (store, _tmp) = test_store(250);
let owned_hash = add_blob(&store, &vec![7u8; 100]);
store
.set_blob_owner(&owned_hash, &[42u8; 32])
.expect("Failed to set owner");
let disposable_hash = add_blob(&store, &vec![1u8; 100]);
let cached_hash = from_hex(&store.put_cached_blob(&vec![2u8; 100]).unwrap()).unwrap();
assert!(
store.blob_exists(&owned_hash).unwrap(),
"Owned Blossom blob should not be evicted by cache cleanup"
);
assert!(
!store.blob_exists(&disposable_hash).unwrap(),
"Unowned orphan should be evicted first"
);
assert!(store.blob_exists(&cached_hash).unwrap());
assert!(used_bytes(&store) <= store.max_size_bytes());
}