#[cfg(feature = "redis")]
mod redis_tests {
use cachekit::backend::redis::RedisBackendBuilder;
use cachekit::backend::{Backend, LockableBackend};
#[test]
fn redis_builder_missing_url() {
let result = RedisBackendBuilder::default().build();
assert!(result.is_err(), "expected error when no URL is provided");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("url is required"),
"unexpected error message: {msg}"
);
}
#[test]
fn redis_builder_from_url() {
let result = RedisBackendBuilder::default()
.url("redis://127.0.0.1:6379")
.build();
assert!(
result.is_ok(),
"expected Ok when a valid URL is provided, got: {result:?}"
);
}
#[tokio::test]
#[allow(clippy::expect_used)] async fn redis_lock_live_semantics() {
let Ok(url) = std::env::var("CACHEKIT_TEST_REDIS_URL") else {
eprintln!("skipping redis_lock_live_semantics: CACHEKIT_TEST_REDIS_URL not set");
return;
};
let backend = RedisBackendBuilder::default()
.url(&url)
.build()
.expect("builder should succeed");
let _handle = backend.connect().await.expect("redis should be reachable");
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
let key = format!("cachekit-test:lock:{nanos}");
let lock_id = backend
.acquire_lock(&key, 30_000)
.await
.expect("acquire should not error")
.expect("first acquire should win");
let raw = backend
.get(&format!("{key}:lock"))
.await
.expect("get should not error")
.expect("lock key should exist on the wire");
assert_eq!(raw, lock_id.as_bytes());
let contested = backend
.acquire_lock(&key, 30_000)
.await
.expect("contested acquire should not error");
assert!(contested.is_none(), "contested acquire must return None");
let released = backend
.release_lock(&key, "not-the-token")
.await
.expect("release should not error");
assert!(!released, "release with a foreign token must be a no-op");
let released = backend
.release_lock(&key, &lock_id)
.await
.expect("release should not error");
assert!(released, "holder release must succeed");
let released = backend
.release_lock(&key, &lock_id)
.await
.expect("release should not error");
assert!(!released, "double release must return false");
let reacquired = backend
.acquire_lock(&key, 100)
.await
.expect("acquire should not error");
assert!(reacquired.is_some(), "lock must be free after release");
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let after_expiry = backend
.acquire_lock(&key, 30_000)
.await
.expect("acquire should not error")
.expect("lock must self-release after timeout_ms");
backend
.release_lock(&key, &after_expiry)
.await
.expect("cleanup release should not error");
}
fn spawn_silent_redis() -> std::net::SocketAddr {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind stub");
let addr = listener.local_addr().expect("stub addr");
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { return };
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
while let Ok(n) = stream.read(&mut buf) {
if n == 0 {
return;
}
let cmd = String::from_utf8_lossy(&buf[..n]).to_ascii_uppercase();
let reply: &[u8] = if cmd.contains("PING") {
b"+PONG\r\n"
} else if cmd.contains("CLIENT") || cmd.contains("INFO") {
b"-ERR stub\r\n"
} else {
continue; };
if stream.write_all(reply).is_err() {
return;
}
}
});
}
});
addr
}
#[tokio::test]
#[allow(clippy::expect_used)] async fn redis_ops_time_out_against_unresponsive_server() {
use cachekit::error::BackendErrorKind;
use std::time::{Duration, Instant};
let addr = spawn_silent_redis();
let backend = RedisBackendBuilder::default()
.url(format!("redis://{addr}"))
.build()
.expect("builder should succeed");
tokio::time::timeout(Duration::from_secs(30), async {
let _handle = backend
.connect()
.await
.expect("stub handshake should succeed");
let start = Instant::now();
let (get, set, lock) = tokio::join!(
backend.get("k"),
backend.set("k", b"v".to_vec(), None),
backend.acquire_lock("k", 1_000),
);
let elapsed = start.elapsed();
assert_eq!(
get.expect_err("get must time out").kind,
BackendErrorKind::Timeout
);
assert_eq!(
set.expect_err("set must time out").kind,
BackendErrorKind::Timeout
);
assert_eq!(
lock.expect_err("acquire_lock must time out").kind,
BackendErrorKind::Timeout
);
assert!(elapsed < Duration::from_secs(6), "ops took {elapsed:?}");
})
.await
.expect("ops hung: no command timeout on the fred client");
}
}