use super::Cache;
use crate::oid::Oid;
fn oid(seed: u8) -> Oid {
Oid::parse(&hex::encode([seed; 32])).expect("a digest")
}
fn cache(root: &tempfile::TempDir, ceiling: u64) -> Cache {
Cache::new(root.path().join("cache"), ceiling).expect("a cache directory")
}
async fn fill(cache: &Cache, oid: &Oid, bytes: &[u8]) {
let chunk = axum::body::Bytes::copy_from_slice(bytes);
cache
.fill(
oid,
bytes.len() as u64,
futures_util::stream::once(async { Ok(chunk) }),
)
.await
.expect("the object is cached");
}
async fn read(file: tokio::fs::File) -> Vec<u8> {
use tokio::io::AsyncReadExt;
let mut bytes = Vec::new();
tokio::io::BufReader::new(file)
.read_to_end(&mut bytes)
.await
.expect("the cached file reads");
bytes
}
#[tokio::test]
async fn an_object_comes_back_byte_for_byte() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 1 << 20);
let payload = b"a mesh nobody wants to fetch twice".repeat(64);
fill(&cache, &oid(1), &payload).await;
let cached = cache.open(&oid(1)).await.expect("the object is cached");
assert_eq!(read(cached).await, payload);
}
#[tokio::test]
async fn an_object_that_was_never_cached_is_a_miss() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 1 << 20);
assert!(cache.open(&oid(2)).await.is_none());
assert_eq!(cache.stats().misses, 1);
}
#[tokio::test]
async fn a_corrupted_entry_is_discarded_rather_than_served() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 1 << 20);
let payload = b"an asset that will be tampered with".to_vec();
fill(&cache, &oid(3), &payload).await;
let path = root.path().join("cache").join(oid(3).to_string());
tokio::fs::write(&path, b"not what was stored")
.await
.unwrap();
assert!(
cache.open(&oid(3)).await.is_none(),
"a cached file that fails its digest must never reach a client"
);
assert!(
!path.exists(),
"and it must be gone, so the next reader fills it from the bucket again"
);
}
#[tokio::test]
async fn a_truncated_entry_is_discarded_too() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 1 << 20);
fill(&cache, &oid(4), &b"a whole object".repeat(32)).await;
let path = root.path().join("cache").join(oid(4).to_string());
tokio::fs::write(&path, b"a whole").await.unwrap();
assert!(cache.open(&oid(4)).await.is_none());
}
#[tokio::test]
async fn the_ceiling_evicts_what_was_used_longest_ago() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 200);
let payload = [7u8; 100];
fill(&cache, &oid(5), &payload).await;
fill(&cache, &oid(6), &payload).await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(cache.open(&oid(5)).await.is_some());
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
fill(&cache, &oid(7), &payload).await;
assert!(
cache.open(&oid(5)).await.is_some(),
"the object somebody just read is the one worth keeping"
);
assert!(
cache.open(&oid(6)).await.is_none(),
"and the one nobody has touched since it arrived is the one to drop"
);
assert!(cache.stats().bytes <= 200);
}
#[tokio::test]
async fn only_one_caller_fills_an_object() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 1 << 20);
assert!(cache.claim(&oid(8)));
assert!(
!cache.claim(&oid(8)),
"the second reader leaves the fetch to the first"
);
cache.release(&oid(8));
assert!(
cache.claim(&oid(8)),
"and once it is done the object can be filled again"
);
}
#[tokio::test]
async fn an_object_larger_than_the_ceiling_is_not_worth_caching() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 100);
assert!(cache.fits(100));
assert!(!cache.fits(101));
}
#[tokio::test]
async fn a_short_body_is_refused_rather_than_cached() {
let root = tempfile::tempdir().unwrap();
let cache = cache(&root, 1 << 20);
let chunk = axum::body::Bytes::from_static(b"only half of it");
let filled = cache
.fill(
&oid(9),
1024,
futures_util::stream::once(async { Ok(chunk) }),
)
.await;
assert!(filled.is_err());
assert!(cache.open(&oid(9)).await.is_none());
assert_eq!(cache.stats().bytes, 0);
}