use super::super::*;
use super::*;
use crate::storage::cas::CasStore;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};
fn fixture_manifest_with_hashes(hashes: [[u8; 32]; 5]) -> Manifest {
let mut layers = HashMap::new();
layers.insert(LayerKind::Db, hashes[0]);
layers.insert(LayerKind::Tfidf, hashes[1]);
layers.insert(LayerKind::Neural, hashes[2]);
layers.insert(LayerKind::Pdg, hashes[3]);
layers.insert(LayerKind::Symbols, hashes[4]);
Manifest {
version: MANIFEST_VERSION,
generation: 1,
model_identity: ModelIdentity {
name: "test".to_string(),
digest: "sha256:test".to_string(),
dimensions: 384,
},
graph_fingerprint: [0x11; 32],
search_fingerprint: [0x22; 32],
layers,
}
}
fn setup_store_with_blobs() -> (tempfile::TempDir, Arc<Mutex<CasStore>>, Vec<[u8; 32]>) {
let dir = tempfile::tempdir().expect("tempdir");
let store = Arc::new(Mutex::new(CasStore::open(dir.path()).expect("open cas")));
let payloads: &[&[u8]] = &[
b"db layer blob",
b"tfidf layer blob",
b"neural layer blob",
b"pdg layer blob",
b"symbols layer blob",
];
let mut hashes = Vec::new();
{
let s = store.lock().unwrap();
for payload in payloads {
let h = s.put(payload).expect("put");
hashes.push(h);
}
}
let hash_arr: [[u8; 32]; 5] = [hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]];
let _manifest = fixture_manifest_with_hashes(hash_arr);
(dir, store, hashes)
}
#[test]
fn test_lease_acquires_and_increments_refcounts() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0);
}
}
let lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 1, "refcount must be incremented on acquire");
}
}
assert_eq!(lease.generation(), manifest.generation);
}
#[test]
fn test_lease_drop_decrements_refcounts() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
{
let s = store.lock().unwrap();
assert_eq!(s.refcount(&hashes[0]), 0);
}
{
let _lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
let s = store.lock().unwrap();
assert_eq!(s.refcount(&hashes[0]), 1);
}
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0, "refcount must be decremented on drop");
}
}
}
#[test]
fn test_lease_prevents_gc() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
let lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
{
let mut s = store.lock().unwrap();
let report = s.gc().expect("gc");
assert_eq!(
report.blobs_removed, 0,
"no blobs collected while lease held"
);
}
{
let s = store.lock().unwrap();
for h in &hashes {
assert!(s.exists(h), "blob must survive GC while lease held");
}
}
drop(lease);
{
let mut s = store.lock().unwrap();
let report = s.gc().expect("gc after drop");
assert_eq!(
report.blobs_removed, 5,
"all blobs collectible after lease dropped"
);
}
}
#[test]
fn test_multiple_leases_same_generation() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
let lease1 = GenerationLease::acquire(store.clone(), &manifest).expect("acquire 1");
let lease2 = GenerationLease::acquire(store.clone(), &manifest).expect("acquire 2");
let lease3 = GenerationLease::acquire(store.clone(), &manifest).expect("acquire 3");
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 3);
}
}
drop(lease1);
drop(lease2);
{
let mut s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 1);
}
let report = s.gc().expect("gc");
assert_eq!(report.blobs_removed, 0);
}
drop(lease3);
{
let mut s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0);
}
let report = s.gc().expect("gc final");
assert_eq!(report.blobs_removed, 5);
}
}
#[test]
fn test_lease_no_leindex_lock_required() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
let lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
assert_eq!(lease.generation(), 1);
assert_eq!(lease.layer_hashes().len(), 5);
{
let s = store.lock().unwrap();
let data = s.get(&hashes[0]).expect("get db blob");
assert_eq!(data, b"db layer blob");
}
drop(lease);
}
#[test]
fn test_lease_provides_manifest_access() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
let lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
let m = lease.manifest();
assert_eq!(m.generation, 1);
assert_eq!(m.version, MANIFEST_VERSION);
assert_eq!(m.layers.len(), 5);
drop(lease);
}
#[test]
fn test_lease_acquire_persists_refcounts() {
let dir = tempfile::tempdir().expect("tempdir");
let store = Arc::new(Mutex::new(CasStore::open(dir.path()).expect("open")));
let mut hashes = Vec::new();
{
let s = store.lock().unwrap();
for i in 0..5u8 {
hashes.push(s.put(&[i; 32]).expect("put"));
}
}
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
{
let _lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
}
{
let s = CasStore::open(dir.path()).expect("reopen");
for h in &hashes {
assert_eq!(s.refcount(h), 0);
}
}
}
#[test]
#[cfg(unix)]
fn test_lease_acquire_persist_failure_rolls_back_deltas_and_reports() {
use std::os::unix::fs::PermissionsExt;
let (dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
let restore_permissions = |path: &Path| {
let mut perms = std::fs::metadata(path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).unwrap();
};
let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
perms.set_mode(0o555);
std::fs::set_permissions(dir.path(), perms).unwrap();
let probe = dir.path().join(".write-probe");
if std::fs::write(&probe, b"x").is_ok() {
let _ = std::fs::remove_file(&probe);
restore_permissions(dir.path());
return;
}
let result = GenerationLease::acquire(store.clone(), &manifest);
restore_permissions(dir.path());
match result {
Err(LeaseError::Persist(_)) => {}
Err(other) => panic!("expected LeaseError::Persist, got: {other}"),
Ok(_) => panic!("acquire unexpectedly succeeded despite the unwritable sidecar"),
}
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0, "rollback must undo the unpersisted incr");
}
assert!(
!s.held_generations().contains(&manifest.generation),
"rollback must release the generation hold"
);
}
{
let lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire after fix");
assert_eq!(lease.generation(), manifest.generation);
drop(lease);
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0);
}
}
}
fn write_generation_to_disk(storage_root: &Path, manifest: &Manifest) -> Vec<[u8; 32]> {
let gen_dir = storage_root
.join("generations")
.join(manifest.generation.to_string());
std::fs::create_dir_all(&gen_dir).expect("create gen dir");
let manifest_bytes = manifest.to_bytes().expect("serialize");
std::fs::write(gen_dir.join("manifest"), &manifest_bytes).expect("write manifest");
std::fs::write(
storage_root.join("CURRENT"),
format!("{}\n", manifest.generation),
)
.expect("write CURRENT");
manifest.layer_hashes()
}
fn fixture_manifest(generation: u64, hashes: [[u8; 32]; 5]) -> Manifest {
let mut layers = HashMap::new();
layers.insert(LayerKind::Db, hashes[0]);
layers.insert(LayerKind::Tfidf, hashes[1]);
layers.insert(LayerKind::Neural, hashes[2]);
layers.insert(LayerKind::Pdg, hashes[3]);
layers.insert(LayerKind::Symbols, hashes[4]);
Manifest {
version: MANIFEST_VERSION,
generation,
model_identity: ModelIdentity {
name: "test".to_string(),
digest: "sha256:test".to_string(),
dimensions: 384,
},
graph_fingerprint: [0x11; 32],
search_fingerprint: [0x22; 32],
layers,
}
}
#[test]
fn test_read_current_generation() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("CURRENT"), b"42\n").expect("write");
assert_eq!(read_current_generation(dir.path()), Some(42));
}
#[test]
fn test_read_current_generation_missing() {
let dir = tempfile::tempdir().expect("tempdir");
assert_eq!(read_current_generation(dir.path()), None);
}
#[test]
fn test_read_current_generation_non_numeric() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("CURRENT"), b"gen-abc\n").expect("write");
assert_eq!(read_current_generation(dir.path()), None);
}
#[test]
fn test_read_generation_manifest_from_disk() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = fixture_manifest(
7,
[[0xAA; 32], [0xBB; 32], [0xCC; 32], [0xDD; 32], [0xEE; 32]],
);
write_generation_to_disk(dir.path(), &manifest);
let recovered = read_generation_manifest(dir.path(), 7).expect("read manifest");
assert_eq!(recovered, manifest);
}
#[test]
fn test_read_generation_manifest_missing_file() {
let dir = tempfile::tempdir().expect("tempdir");
let err = read_generation_manifest(dir.path(), 999).unwrap_err();
assert!(matches!(err, ManifestError::MissingManifest { .. }));
}
#[test]
fn test_lease_acquires_from_disk() {
let dir = tempfile::tempdir().expect("tempdir");
let storage_root = dir.path();
let cas_dir = storage_root.join("cas");
let store = CasStore::open(&cas_dir).expect("open cas");
let payloads: &[&[u8]] = &[b"db", b"tfidf", b"neural", b"pdg", b"symbols"];
let mut hashes = Vec::new();
for p in payloads {
hashes.push(store.put(p).expect("put"));
}
drop(store);
let hash_arr: [[u8; 32]; 5] = [hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]];
let manifest = fixture_manifest(3, hash_arr);
write_generation_to_disk(storage_root, &manifest);
let gen_n = read_current_generation(storage_root).expect("read current");
assert_eq!(gen_n, 3);
let m = read_generation_manifest(storage_root, gen_n).expect("read manifest");
let store = Arc::new(Mutex::new(CasStore::open(&cas_dir).expect("reopen cas")));
let lease = GenerationLease::acquire(store.clone(), &m).expect("acquire");
assert_eq!(lease.generation(), 3);
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 1);
}
}
drop(lease);
{
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0);
}
}
}
#[test]
fn test_lease_acquire_reports_generation_pruned_and_rolls_back() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
remove_blob_files(_dir.path());
match GenerationLease::acquire(store.clone(), &manifest) {
Err(LeaseError::GenerationPruned { generation }) => {
assert_eq!(generation, manifest.generation);
}
Err(other) => panic!("expected GenerationPruned, got {other}"),
Ok(_) => panic!("expected GenerationPruned, the acquisition must not succeed"),
}
let s = store.lock().unwrap();
for h in &hashes {
assert_eq!(s.refcount(h), 0, "rollback must undo every incr");
}
assert!(
!s.held_generations().contains(&manifest.generation),
"rollback must release the generation hold"
);
}
#[test]
fn test_lease_acquire_still_succeeds_when_blobs_exist() {
let (_dir, store, hashes) = setup_store_with_blobs();
let manifest =
fixture_manifest_with_hashes([hashes[0], hashes[1], hashes[2], hashes[3], hashes[4]]);
let lease = GenerationLease::acquire(store.clone(), &manifest).expect("acquire");
assert_eq!(lease.generation(), manifest.generation);
}
fn remove_blob_files(root: &Path) {
for entry in std::fs::read_dir(root).expect("cas root") {
let entry = entry.expect("entry");
if !entry.file_type().expect("type").is_dir() {
continue;
}
for blob in std::fs::read_dir(entry.path()).expect("shard") {
let blob = blob.expect("blob entry");
let name = blob.file_name().to_string_lossy().to_string();
if name.len() == 64 && name.bytes().all(|b| b.is_ascii_hexdigit()) {
std::fs::remove_file(blob.path()).expect("remove blob");
}
}
}
}