use crate::models::CacheEntry;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
pub const CACHE_DURATION: Duration = Duration::from_secs(30 * 60);
pub const LIMITE: usize = 512;
pub fn is_expired(timestamp: SystemTime, ahora: SystemTime, duracion: Duration) -> bool {
ahora.duration_since(timestamp).map_or(true, |d| d > duracion)
}
pub fn bloquear<T>(candado: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
candado.lock().unwrap_or_else(|envenenado| envenenado.into_inner())
}
pub fn evict_oldest(cache: &mut HashMap<String, CacheEntry>, limite: usize) {
while cache.len() > limite {
let mas_vieja = cache
.iter()
.min_by_key(|(_, entrada)| entrada.timestamp)
.map(|(clave, _)| clave.clone());
match mas_vieja {
Some(clave) => {
cache.remove(&clave);
}
None => break,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entrada(segundos: u64, datos: &str) -> CacheEntry {
CacheEntry {
data: datos.to_string(),
timestamp: SystemTime::UNIX_EPOCH + Duration::from_secs(segundos),
}
}
#[test]
fn una_entrada_reciente_sigue_valiendo() {
let ahora = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let guardada = SystemTime::UNIX_EPOCH + Duration::from_secs(900);
assert!(!is_expired(guardada, ahora, Duration::from_secs(200)));
}
#[test]
fn una_entrada_vieja_expira() {
let ahora = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let guardada = SystemTime::UNIX_EPOCH + Duration::from_secs(700);
assert!(is_expired(guardada, ahora, Duration::from_secs(200)));
}
#[test]
fn el_borde_exacto_todavia_vale() {
let ahora = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
let guardada = SystemTime::UNIX_EPOCH + Duration::from_secs(800);
assert!(!is_expired(guardada, ahora, Duration::from_secs(200)));
}
#[test]
fn una_fecha_del_futuro_cuenta_como_expirada() {
let ahora = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
let guardada = SystemTime::UNIX_EPOCH + Duration::from_secs(5000);
assert!(is_expired(guardada, ahora, Duration::from_secs(200)));
}
#[test]
fn la_cache_no_pasa_del_techo() {
let mut cache = HashMap::new();
for i in 0..20 {
cache.insert(format!("icono-{i}"), entrada(i, "datos"));
}
evict_oldest(&mut cache, 5);
assert_eq!(cache.len(), 5);
}
#[test]
fn se_saca_primero_lo_mas_viejo() {
let mut cache = HashMap::new();
cache.insert("vieja".to_string(), entrada(10, "a"));
cache.insert("media".to_string(), entrada(20, "b"));
cache.insert("nueva".to_string(), entrada(30, "c"));
evict_oldest(&mut cache, 2);
assert!(!cache.contains_key("vieja"), "salió la más vieja");
assert!(cache.contains_key("media"));
assert!(cache.contains_key("nueva"));
}
#[test]
fn por_debajo_del_techo_no_se_toca_nada() {
let mut cache = HashMap::new();
cache.insert("una".to_string(), entrada(1, "a"));
evict_oldest(&mut cache, 10);
assert_eq!(cache.len(), 1);
}
#[test]
fn un_techo_de_cero_no_deja_el_bucle_colgado() {
let mut cache = HashMap::new();
cache.insert("una".to_string(), entrada(1, "a"));
evict_oldest(&mut cache, 0);
assert!(cache.is_empty());
evict_oldest(&mut cache, 0);
}
#[test]
fn un_candado_envenenado_se_recupera() {
let candado = std::sync::Arc::new(Mutex::new(vec![1, 2, 3]));
let otro = candado.clone();
let _ = std::thread::spawn(move || {
let _guardia = otro.lock().unwrap();
panic!("envenena el candado");
})
.join();
assert!(candado.lock().is_err(), "el candado tiene que estar envenenado");
assert_eq!(*bloquear(&candado), vec![1, 2, 3]);
}
#[test]
fn el_techo_y_la_duracion_son_los_que_se_documentaron() {
assert_eq!(LIMITE, 512);
assert_eq!(CACHE_DURATION, Duration::from_secs(30 * 60));
}
}