use std::sync::Arc; use std::thread; use ontoref_blobs::{BlobId, BlobStore, Error}; #[test] fn put_get_round_trip() { let dir = tempfile::tempdir().unwrap(); let store = BlobStore::open(dir.path()).unwrap(); let id = store.put(b"hello world").unwrap(); assert_eq!(store.get(&id).unwrap(), b"hello world"); } #[test] fn put_get_empty_bytes() { let dir = tempfile::tempdir().unwrap(); let store = BlobStore::open(dir.path()).unwrap(); let id = store.put(b"").unwrap(); assert_eq!(store.get(&id).unwrap(), Vec::::new()); } #[test] fn put_get_is_idempotent() { let dir = tempfile::tempdir().unwrap(); let store = BlobStore::open(dir.path()).unwrap(); let id1 = store.put(b"same content").unwrap(); let id2 = store.put(b"same content").unwrap(); assert_eq!(id1, id2); assert_eq!(store.get(&id1).unwrap(), b"same content"); } #[test] fn put_get_missing_returns_not_found() { let dir = tempfile::tempdir().unwrap(); let store = BlobStore::open(dir.path()).unwrap(); let dummy = BlobId([0u8; 32]); match store.get(&dummy) { Err(Error::NotFound(_)) => {} other => panic!("expected NotFound, got {other:?}"), } } #[test] fn concurrent_puts_of_same_blob() { let dir = tempfile::tempdir().unwrap(); let store = Arc::new(BlobStore::open(dir.path()).unwrap()); let payload = b"shared content".to_vec(); let handles: Vec<_> = (0..16) .map(|_| { let store = Arc::clone(&store); let payload = payload.clone(); thread::spawn(move || store.put(&payload).unwrap()) }) .collect(); let ids: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect(); let first = ids[0]; for id in &ids { assert_eq!(*id, first); } assert_eq!(store.get(&first).unwrap(), payload); } #[test] fn concurrent_puts_of_different_blobs() { let dir = tempfile::tempdir().unwrap(); let store = Arc::new(BlobStore::open(dir.path()).unwrap()); let handles: Vec<_> = (0..32) .map(|i| { let store = Arc::clone(&store); let payload = format!("blob #{i}").into_bytes(); thread::spawn(move || { let id = store.put(&payload).unwrap(); (id, payload) }) }) .collect(); for handle in handles { let (id, payload) = handle.join().unwrap(); assert_eq!(store.get(&id).unwrap(), payload); } } #[test] fn crash_safe_no_orphan_tmp_after_successful_put() { let dir = tempfile::tempdir().unwrap(); let store = BlobStore::open(dir.path()).unwrap(); let id = store.put(b"normal content").unwrap(); let parent = store.path_for(&id).parent().unwrap().to_path_buf(); let entries: Vec = std::fs::read_dir(&parent) .unwrap() .filter_map(|entry| entry.ok()) .map(|entry| entry.file_name().to_string_lossy().into_owned()) .collect(); assert_eq!( entries, vec![id.to_hex()], "blob directory should contain exactly the finalised blob" ); } #[test] fn crash_safe_stale_tmp_does_not_break_get() { let dir = tempfile::tempdir().unwrap(); let store = BlobStore::open(dir.path()).unwrap(); let id = store.put(b"real").unwrap(); let parent = store.path_for(&id).parent().unwrap().to_path_buf(); std::fs::write(parent.join(".tmp.garbage"), b"garbage from a crashed put").unwrap(); assert_eq!(store.get(&id).unwrap(), b"real"); }