#![cfg(feature = "vlm")]
use mlxrs::{
Array,
error::Error,
vlm::feature_cache::{DEFAULT_MAX_SIZE, Key, VisionFeatureCache},
};
fn features(n: usize, d: usize, value: f32) -> Array {
Array::full::<f32>(&(1usize, n, d), value).unwrap()
}
fn contents(mut a: Array) -> Vec<f32> {
a.to_vec::<f32>().unwrap()
}
#[test]
fn put_and_get() {
let mut cache = VisionFeatureCache::with_max_size(2).unwrap();
let f = features(280, 1536, 1.0);
cache.put(Key::from_source("image1.jpg"), &f).unwrap();
let got = cache.get(&Key::from_source("image1.jpg")).unwrap();
assert!(got.is_some(), "expected a cache hit for image1.jpg");
let got = got.unwrap();
assert_eq!(got.shape(), vec![1, 280, 1536]);
assert_eq!(contents(got), contents(f));
}
#[test]
fn cache_miss() {
let mut cache = VisionFeatureCache::new();
let got = cache.get(&Key::from_source("nonexistent.jpg")).unwrap();
assert!(got.is_none(), "absent key must miss");
}
#[test]
fn lru_eviction() {
let mut cache = VisionFeatureCache::with_max_size(2).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("b.jpg"), &features(10, 64, 2.0))
.unwrap();
cache
.put(Key::from("c.jpg"), &features(10, 64, 3.0))
.unwrap();
assert!(
cache.get(&Key::from("a.jpg")).unwrap().is_none(),
"a.jpg should have been evicted as the LRU entry"
);
assert!(cache.get(&Key::from("b.jpg")).unwrap().is_some());
assert!(cache.get(&Key::from("c.jpg")).unwrap().is_some());
assert_eq!(cache.len(), 2, "capacity stays bounded at max_size");
}
#[test]
fn lru_touch_refreshes_recency() {
let mut cache = VisionFeatureCache::with_max_size(2).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("b.jpg"), &features(10, 64, 2.0))
.unwrap();
assert!(cache.get(&Key::from("a.jpg")).unwrap().is_some());
cache
.put(Key::from("c.jpg"), &features(10, 64, 3.0))
.unwrap(); assert!(
cache.get(&Key::from("a.jpg")).unwrap().is_some(),
"a was touched and must survive"
);
assert!(
cache.get(&Key::from("b.jpg")).unwrap().is_none(),
"b was the LRU after a's touch and must be evicted"
);
assert!(cache.get(&Key::from("c.jpg")).unwrap().is_some());
}
#[test]
fn distinct_keys_do_not_collide() {
let mut cache = VisionFeatureCache::with_max_size(4).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("b.jpg"), &features(10, 64, 2.0))
.unwrap();
cache
.put(Key::from("c.jpg"), &features(10, 64, 3.0))
.unwrap();
assert_eq!(
contents(cache.get(&Key::from("a.jpg")).unwrap().unwrap())[0],
1.0
);
assert_eq!(
contents(cache.get(&Key::from("b.jpg")).unwrap().unwrap())[0],
2.0
);
assert_eq!(
contents(cache.get(&Key::from("c.jpg")).unwrap().unwrap())[0],
3.0
);
assert_eq!(cache.len(), 3);
}
#[test]
fn multi_image_key_order_matters() {
let mut cache = VisionFeatureCache::new();
let f = features(560, 1536, 1.0);
cache
.put(Key::from_sources(&["img1.jpg", "img2.jpg"]), &f)
.unwrap();
assert!(
cache
.get(&Key::from_sources(&["img1.jpg", "img2.jpg"]))
.unwrap()
.is_some(),
"same-order multi-image key must hit"
);
assert!(
cache
.get(&Key::from_sources(&["img2.jpg", "img1.jpg"]))
.unwrap()
.is_none(),
"reversed-order multi-image key must miss (order is significant)"
);
}
#[test]
fn from_sources_is_stable_and_order_sensitive() {
assert_eq!(
Key::from_sources(&["a", "b"]),
Key::from_sources(&["a", "b"])
);
assert_ne!(
Key::from_sources(&["a", "b"]),
Key::from_sources(&["b", "a"])
);
assert_eq!(Key::from_sources(&[]), Key::from_sources(&[]));
}
#[test]
fn key_encoding_non_aliasing() {
assert_ne!(
Key::from_source("a|b"),
Key::from_sources(&["a", "b"]),
"a literal '|'-bearing path must NOT alias a 2-element list key"
);
assert_ne!(
Key::from_sources(&["a|b"]),
Key::from_sources(&["a", "b"]),
"['a|b'] (one component) must NOT alias ['a','b'] (two components)"
);
assert_ne!(
Key::from_sources(&["", "a|b"]),
Key::from_sources(&["", "a", "b"]),
);
assert_ne!(
Key::from_sources(&["a", "", "b"]),
Key::from_sources(&["a", "b"]),
"an empty component must not vanish (it is length-prefixed '0:')"
);
for bytes in [
&b""[..],
&b"deadbeef"[..],
&b"\x00\x01\x02"[..],
&[0xffu8; 32][..],
] {
let hashed = Key::from_bytes(bytes);
let digest = hashed.as_str(); assert_ne!(
Key::from_source(digest),
hashed,
"a literal path == the encoded digest must not alias from_bytes"
);
let payload = digest
.strip_prefix("b:")
.expect("from_bytes key is b:-tagged");
assert_ne!(
Key::from_source(payload),
hashed,
"a literal path == the bare digest payload must not alias from_bytes"
);
}
assert_ne!(
Key::from_source("l:1:a"),
Key::from_sources(&["a"]),
"a source literally 'l:1:a' is an s: key, not the list key l:1:a"
);
assert_ne!(
Key::from_source("b:0000000000000000"),
Key::from_bytes(b"anything"),
"a source shaped like a b: key is an s: key, not a from_bytes key"
);
assert_ne!(Key::from_source("s:foo"), Key::from_source("foo"));
assert_eq!(Key::from_source("img.jpg"), Key::from_source("img.jpg"));
assert_eq!(Key::from_bytes(b"abc"), Key::from_bytes(b"abc"));
assert_eq!(
Key::from_sources(&["a|b", "c"]),
Key::from_sources(&["a|b", "c"])
);
assert_ne!(Key::from_source("x"), Key::from_source("y"));
assert_ne!(Key::from_bytes(b"abc"), Key::from_bytes(b"abd"));
}
#[test]
fn cache_does_not_serve_aliased_keys() {
let mut cache = VisionFeatureCache::with_max_size(8).unwrap();
cache
.put(Key::from_source("a|b"), &features(10, 64, 1.0))
.unwrap();
assert!(
cache
.get(&Key::from_sources(&["a", "b"]))
.unwrap()
.is_none(),
"the 2-element list key must MISS — it must not alias from_source('a|b')"
);
cache
.put(Key::from_sources(&["a|b"]), &features(10, 64, 2.0))
.unwrap();
assert!(
cache
.get(&Key::from_sources(&["a", "b"]))
.unwrap()
.is_none(),
"['a','b'] must MISS — it must not alias the stored ['a|b']"
);
let f = features(10, 64, 3.0);
cache.put(Key::from_bytes(b"deadbeef"), &f).unwrap();
let digest = Key::from_bytes(b"deadbeef").as_str().to_owned();
assert!(
cache.get(&Key::from_source(&digest)).unwrap().is_none(),
"a literal path == the encoded digest must MISS"
);
let payload = digest.strip_prefix("b:").unwrap().to_owned();
assert!(
cache.get(&Key::from_source(&payload)).unwrap().is_none(),
"a literal path == the bare digest payload must MISS"
);
assert!(
cache.get(&Key::from_source("a|b")).unwrap().is_some(),
"the original from_source('a|b') key must still hit"
);
assert!(
cache.get(&Key::from_bytes(b"deadbeef")).unwrap().is_some(),
"the original from_bytes key must still hit"
);
}
#[test]
fn url_key() {
let mut cache = VisionFeatureCache::new();
let url = "https://example.com/image.jpg";
cache
.put(Key::from(url), &features(280, 1536, 1.0))
.unwrap();
assert!(cache.get(&Key::from(url)).unwrap().is_some());
}
#[test]
fn content_hash_key() {
let mut cache = VisionFeatureCache::new();
let img_a = [1u8, 2, 3, 4, 5];
let img_b = [9u8, 8, 7, 6, 5];
cache
.put(Key::from_bytes(&img_a), &features(10, 64, 1.0))
.unwrap();
assert!(
cache
.get(&Key::from_bytes(&[1u8, 2, 3, 4, 5]))
.unwrap()
.is_some()
);
assert!(cache.get(&Key::from_bytes(&img_b)).unwrap().is_none());
assert!(Key::from_bytes(&img_a).as_str().starts_with("b:"));
}
#[test]
fn from_bytes_is_stable_fixed_width_digest() {
assert_eq!(Key::from_bytes(b"abc"), Key::from_bytes(b"abc"));
assert_eq!(Key::from_bytes(b""), Key::from_bytes(b""));
assert_eq!(
Key::from_bytes(&[0xffu8; 64]),
Key::from_bytes(&[0xffu8; 64])
);
for bytes in [
&b""[..],
&b"x"[..],
&b"a longer payload than one hash block, exercising the streaming path"[..],
&[0u8; 1024][..],
] {
let key = Key::from_bytes(bytes);
let s = key.as_str();
assert_eq!(s.len(), 34, "b: + 32 hex chars (128-bit digest), got {s:?}");
let payload = s.strip_prefix("b:").expect("from_bytes key is b:-tagged");
assert_eq!(payload.len(), 32, "128-bit digest is 32 hex chars");
assert!(
payload.bytes().all(|c| c.is_ascii_hexdigit()),
"digest payload must be lowercase hex, got {payload:?}"
);
}
let samples: [&[u8]; 6] = [b"", b"a", b"b", b"ab", b"ba", &[0u8; 5]];
for (i, &x) in samples.iter().enumerate() {
for &y in samples.iter().skip(i + 1) {
assert_ne!(
Key::from_bytes(x),
Key::from_bytes(y),
"distinct byte inputs must digest to distinct 128-bit keys"
);
}
}
assert_ne!(Key::from_bytes(b"\x00"), Key::from_bytes(b"\x01"));
}
#[test]
fn clear_empties_and_releases_all() {
let mut cache = VisionFeatureCache::with_max_size(8).unwrap();
for i in 0..5 {
cache
.put(
Key::from(format!("img{i}.jpg").as_str()),
&features(10, 64, i as f32),
)
.unwrap();
}
assert_eq!(cache.len(), 5);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert_eq!(cache.max_size(), 8, "max_size survives clear");
for i in 0..5 {
assert!(
cache
.get(&Key::from(format!("img{i}.jpg").as_str()))
.unwrap()
.is_none(),
"every cleared key must miss"
);
}
}
#[test]
fn contains_reports_membership() {
let mut cache = VisionFeatureCache::new();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
assert!(cache.contains(&Key::from("a.jpg")));
assert!(!cache.contains(&Key::from("b.jpg")));
}
#[test]
fn contains_does_not_refresh_recency() {
let mut cache = VisionFeatureCache::with_max_size(2).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("b.jpg"), &features(10, 64, 2.0))
.unwrap();
assert!(cache.contains(&Key::from("a.jpg")));
cache
.put(Key::from("c.jpg"), &features(10, 64, 3.0))
.unwrap();
assert!(
cache.get(&Key::from("a.jpg")).unwrap().is_none(),
"contains must not have refreshed a's recency; a stays the LRU"
);
assert!(cache.get(&Key::from("b.jpg")).unwrap().is_some());
}
#[test]
fn overwrite_existing_key() {
let mut cache = VisionFeatureCache::with_max_size(2).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 5.0))
.unwrap();
assert_eq!(cache.len(), 1, "overwrite must not grow the cache");
let got = cache.get(&Key::from("a.jpg")).unwrap().unwrap();
assert_eq!(contents(got)[0], 5.0, "value must be the overwritten one");
}
#[test]
fn overwrite_refreshes_recency() {
let mut cache = VisionFeatureCache::with_max_size(2).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("b.jpg"), &features(10, 64, 2.0))
.unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 9.0))
.unwrap();
cache
.put(Key::from("c.jpg"), &features(10, 64, 3.0))
.unwrap(); assert!(cache.get(&Key::from("a.jpg")).unwrap().is_some());
assert!(
cache.get(&Key::from("b.jpg")).unwrap().is_none(),
"b was the LRU after a's overwrite and must be evicted"
);
}
#[test]
fn default_max_size_is_20() {
let cache = VisionFeatureCache::new();
assert_eq!(cache.max_size(), 20);
assert_eq!(DEFAULT_MAX_SIZE, 20);
assert_eq!(VisionFeatureCache::default().max_size(), 20);
}
#[test]
fn zero_max_size_is_rejected() {
let err = VisionFeatureCache::with_max_size(0).unwrap_err();
assert!(
matches!(err, Error::InvariantViolation(_)),
"zero capacity must be an InvariantViolation error, got {err:?}"
);
}
#[test]
fn with_max_size_pathological_capacity_is_cheap_not_abort() {
let cache = VisionFeatureCache::with_max_size(usize::MAX)
.expect("usize::MAX max_size must construct (empty-init reserves nothing)");
assert_eq!(cache.len(), 0, "no entries are pre-allocated");
assert_eq!(
cache.max_size(),
usize::MAX,
"the requested (pathological) max_size is kept as the eviction bound"
);
}
#[test]
fn with_max_size_large_value_does_not_eagerly_allocate() {
let big = 1usize << 40;
let cache = VisionFeatureCache::with_max_size(big)
.expect("a large max_size must construct cheaply with no upfront reserve");
assert_eq!(cache.len(), 0, "empty-init: no entries pre-allocated");
assert!(cache.is_empty());
assert_eq!(
cache.max_size(),
big,
"the requested max_size is the (lazily applied) eviction bound"
);
}
#[test]
fn fill_to_capacity_no_eviction() {
let mut cache = VisionFeatureCache::with_max_size(3).unwrap();
cache
.put(Key::from("a.jpg"), &features(10, 64, 1.0))
.unwrap();
cache
.put(Key::from("b.jpg"), &features(10, 64, 2.0))
.unwrap();
cache
.put(Key::from("c.jpg"), &features(10, 64, 3.0))
.unwrap();
assert_eq!(cache.len(), 3);
assert!(cache.contains(&Key::from("a.jpg")));
assert!(cache.contains(&Key::from("b.jpg")));
assert!(cache.contains(&Key::from("c.jpg")));
}