use cutile_compiler::jit_cache::{disable, enable, is_enabled, FileSystemJitStore, JitStore};
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
fn key(tag: u8) -> String {
format!("{tag:02x}").repeat(32)
}
struct TestDir(PathBuf);
impl TestDir {
fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"cutile_jit_cache_test_{}_{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
));
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn put_get_roundtrip_and_overwrite() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
let k = key(0xab);
assert_eq!(store.get(&k).unwrap(), None);
assert!(!store.contains(&k).unwrap());
store.put(&k, b"first").unwrap();
assert_eq!(store.get(&k).unwrap().as_deref(), Some(&b"first"[..]));
assert!(store.contains(&k).unwrap());
store.put(&k, b"second").unwrap();
assert_eq!(store.get(&k).unwrap().as_deref(), Some(&b"second"[..]));
}
#[test]
fn large_value_roundtrip() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
let k = key(0x01);
let value: Vec<u8> = (0..1_000_000u32).map(|i| i as u8).collect();
store.put(&k, &value).unwrap();
assert_eq!(store.get(&k).unwrap(), Some(value));
}
#[test]
fn delete_is_idempotent() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
let k = key(0x02);
store.put(&k, b"v").unwrap();
store.delete(&k).unwrap();
assert_eq!(store.get(&k).unwrap(), None);
store.delete(&k).unwrap();
}
#[test]
fn clear_removes_all_entries() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
let (a, b) = (key(0x03), key(0xf3));
store.put(&a, b"va").unwrap();
store.put(&b, b"vb").unwrap();
store.clear().unwrap();
assert_eq!(store.get(&a).unwrap(), None);
assert_eq!(store.get(&b).unwrap(), None);
store.put(&a, b"va2").unwrap();
assert_eq!(store.get(&a).unwrap().as_deref(), Some(&b"va2"[..]));
}
#[test]
fn rejects_keys_that_are_not_64_hex_chars() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
let bad_keys: Vec<String> = vec![
String::new(),
"short".to_string(),
"../../../../etc/passwd".to_string(),
key(0xab)[..63].to_string(),
format!("{}G", &key(0xab)[..63]), key(0xab).to_uppercase(), ];
for bad in &bad_keys {
let err = store.put(bad, b"v").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput, "key {bad:?}");
assert!(store.get(bad).is_err(), "key {bad:?}");
}
assert!(!dir.0.join("..").join("passwd").exists());
}
#[test]
fn no_temp_files_left_behind() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
for tag in 0..8u8 {
store.put(&key(tag), &[tag; 128]).unwrap();
}
let mut stack = vec![dir.0.clone()];
while let Some(d) = stack.pop() {
for entry in std::fs::read_dir(&d).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
stack.push(entry.path());
} else {
let name = entry.file_name();
let name = name.to_string_lossy();
assert!(
!name.contains(".tmp."),
"temp file leaked: {}",
entry.path().display()
);
}
}
}
}
#[test]
fn concurrent_put_get_is_atomic() {
let dir = TestDir::new();
let store = Arc::new(FileSystemJitStore::new(&dir.0).unwrap());
let k = key(0x77);
let value_a = vec![0xaa; 64 * 1024];
let value_b = vec![0xbb; 64 * 1024];
let mut handles = Vec::new();
for i in 0..8 {
let store = Arc::clone(&store);
let k = k.clone();
let (va, vb) = (value_a.clone(), value_b.clone());
handles.push(std::thread::spawn(move || {
for round in 0..50 {
if i % 2 == 0 {
let v = if (i + round) % 2 == 0 { &va } else { &vb };
store.put(&k, v).unwrap();
} else if let Some(got) = store.get(&k).unwrap() {
assert!(
got == va || got == vb,
"read a value that is neither complete write (len {})",
got.len()
);
}
}
}));
}
for h in handles {
h.join().unwrap();
}
}
#[test]
fn enable_disable_is_repeatable() {
static SLOT_LOCK: Mutex<()> = Mutex::new(());
let _guard = SLOT_LOCK.lock().unwrap();
struct MockStore;
impl JitStore for MockStore {
fn get(&self, _: &str) -> io::Result<Option<Vec<u8>>> {
Ok(None)
}
fn put(&self, _: &str, _: &[u8]) -> io::Result<()> {
Ok(())
}
fn delete(&self, _: &str) -> io::Result<()> {
Ok(())
}
fn clear(&self) -> io::Result<()> {
Ok(())
}
}
assert!(!is_enabled());
enable(Arc::new(MockStore));
assert!(is_enabled());
enable(Arc::new(MockStore)); assert!(is_enabled());
disable();
assert!(!is_enabled());
disable(); assert!(!is_enabled());
}
use cutile_compiler::jit_cache::EVICTION_LOCK_FILE_NAME;
use std::time::{Duration, SystemTime};
fn entry_path(root: &std::path::Path, k: &str) -> PathBuf {
root.join(&k[..2]).join(format!("{k}.cubin"))
}
fn set_mtime(path: &std::path::Path, t: SystemTime) {
let f = std::fs::File::options().write(true).open(path).unwrap();
f.set_times(std::fs::FileTimes::new().set_modified(t))
.unwrap();
}
fn mtime(path: &std::path::Path) -> SystemTime {
std::fs::metadata(path).unwrap().modified().unwrap()
}
fn count_cubin_files(root: &std::path::Path) -> usize {
let mut n = 0;
for shard in std::fs::read_dir(root).unwrap().flatten() {
if shard.file_type().unwrap().is_dir() {
for f in std::fs::read_dir(shard.path()).unwrap().flatten() {
if f.file_name().to_string_lossy().ends_with(".cubin") {
n += 1;
}
}
}
}
n
}
#[test]
fn lru_evicts_oldest_entries_first() {
let dir = TestDir::new();
let seed = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(0)
.open()
.unwrap();
let now = SystemTime::now();
for tag in 0..8u8 {
seed.put(&key(tag), &vec![tag; 2048]).unwrap();
set_mtime(
&entry_path(&dir.0, &key(tag)),
now - Duration::from_secs((100 - u64::from(tag)) * 60),
);
}
let store = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(8192)
.open()
.unwrap();
store.put(&key(0xff), &[0u8; 600]).unwrap();
for tag in 0..6u8 {
assert_eq!(
store.get(&key(tag)).unwrap(),
None,
"entry {tag} is among the oldest and must be evicted"
);
}
for tag in 6..8u8 {
assert!(
store.get(&key(tag)).unwrap().is_some(),
"entry {tag} is recent enough to survive"
);
}
assert!(
store.get(&key(0xff)).unwrap().is_some(),
"the entry just written is the newest and must survive"
);
}
#[test]
fn capacity_holds_across_short_lived_processes() {
let dir = TestDir::new();
const ENTRY_BYTES: usize = 200;
const CAPACITY: u64 = 8192; const PUTS: u8 = 200;
for tag in 0..PUTS {
let store = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(CAPACITY)
.open()
.unwrap();
store.put(&key(tag), &vec![tag; ENTRY_BYTES]).unwrap();
}
let files = count_cubin_files(&dir.0);
assert!(
files < 100,
"cache must stay bounded across short-lived processes, found {files} entries \
({} bytes) with a {CAPACITY}-byte capacity; a per-process trigger leaves all \
{PUTS}",
files * ENTRY_BYTES,
);
}
#[test]
fn oversized_entry_is_declined_and_preserves_cache() {
let dir = TestDir::new();
let seed = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(0)
.open()
.unwrap();
seed.put(&key(0x01), &vec![0x01; 500]).unwrap();
seed.put(&key(0x02), &vec![0x02; 500]).unwrap();
let store = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(8192)
.open()
.unwrap();
store.put(&key(0xff), &vec![0u8; 7000]).unwrap();
assert_eq!(
store.get(&key(0xff)).unwrap(),
None,
"an entry above the low watermark must not be stored"
);
assert!(
store.get(&key(0x01)).unwrap().is_some(),
"the oversized put must not wipe existing entries"
);
assert!(
store.get(&key(0x02)).unwrap().is_some(),
"the oversized put must not wipe existing entries"
);
}
#[test]
fn get_refreshes_entry_mtime() {
let dir = TestDir::new();
let store = FileSystemJitStore::new(&dir.0).unwrap();
let k = key(0x21);
store.put(&k, b"v").unwrap();
let path = entry_path(&dir.0, &k);
set_mtime(&path, SystemTime::now() - Duration::from_secs(7200));
let aged = mtime(&path);
store.get(&k).unwrap().unwrap();
let refreshed = mtime(&path);
assert!(
refreshed
.duration_since(aged)
.is_ok_and(|d| d > Duration::from_secs(7000)),
"hit must move mtime from 2h ago to now"
);
}
#[test]
fn eviction_skips_while_lock_is_held() {
let dir = TestDir::new();
let seed = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(0)
.open()
.unwrap();
let now = SystemTime::now();
for tag in 0..10u8 {
seed.put(&key(tag), &vec![tag; 1024]).unwrap();
set_mtime(
&entry_path(&dir.0, &key(tag)),
now - Duration::from_secs((100 - u64::from(tag)) * 60),
);
}
let store = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(4096)
.open()
.unwrap();
let lock = std::fs::File::create(dir.0.join(EVICTION_LOCK_FILE_NAME)).unwrap();
lock.try_lock().unwrap();
store.put(&key(0xf0), &[0u8; 512]).unwrap();
assert_eq!(
count_cubin_files(&dir.0),
11,
"with the lock held elsewhere, nothing may be deleted"
);
drop(lock);
store.put(&key(0xf1), &[0u8; 512]).unwrap();
assert!(
count_cubin_files(&dir.0) < 12,
"with the lock free, the over-capacity store must shrink"
);
}
#[test]
fn eviction_removes_stale_temp_files_only() {
let dir = TestDir::new();
let store = FileSystemJitStore::builder(&dir.0)
.capacity_bytes(1_000_000)
.open()
.unwrap();
store.put(&key(0x31), b"seed").unwrap();
let shard = dir.0.join(&key(0x31)[..2]);
let stale = shard.join(format!("{}.tmp.999.0", key(0x31)));
let fresh = shard.join(format!("{}.tmp.999.1", key(0x31)));
std::fs::write(&stale, b"crashed process leftover").unwrap();
std::fs::write(&fresh, b"in-flight write").unwrap();
set_mtime(&stale, SystemTime::now() - Duration::from_secs(7200));
store.put(&key(0x32), &vec![0u8; 62_500]).unwrap();
assert!(!stale.exists(), "2h-old temp file must be removed");
assert!(fresh.exists(), "fresh temp file must be left alone");
assert!(store.get(&key(0x31)).unwrap().is_some());
}
#[test]
fn builder_rejects_invalid_watermarks() {
let dir = TestDir::new();
for (high, low) in [(0.5, 0.8), (1.0, 0.0), (1.0, -0.1), (f64::NAN, 0.8)] {
let err = FileSystemJitStore::builder(&dir.0)
.eviction_watermarks(high, low)
.open()
.unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::InvalidInput,
"watermarks ({high}, {low}) must be rejected"
);
}
}