#![allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
hits: u64,
misses: u64,
evictions: u64,
}
impl CacheStats {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_hit(&mut self) {
self.hits = self.hits.saturating_add(1);
}
pub fn record_miss(&mut self) {
self.misses = self.misses.saturating_add(1);
}
pub fn record_eviction(&mut self) {
self.evictions = self.evictions.saturating_add(1);
}
#[must_use]
pub fn hits(&self) -> u64 {
self.hits
}
#[must_use]
pub fn misses(&self) -> u64 {
self.misses
}
#[must_use]
pub fn evictions(&self) -> u64 {
self.evictions
}
#[must_use]
pub fn total_lookups(&self) -> u64 {
self.hits.saturating_add(self.misses)
}
#[must_use]
pub fn hit_rate(&self) -> f32 {
let total = self.total_lookups();
if total == 0 {
return 0.0;
}
self.hits as f32 / total as f32
}
#[must_use]
pub fn miss_rate(&self) -> f32 {
1.0 - self.hit_rate()
}
#[must_use]
pub fn to_json(&self) -> String {
format!(
"{{\"hits\":{},\"misses\":{},\"evictions\":{},\"hit_rate\":{:.6}}}",
self.hits,
self.misses,
self.evictions,
self.hit_rate()
)
}
pub fn reset(&mut self) {
self.hits = 0;
self.misses = 0;
self.evictions = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_starts_at_zero() {
let s = CacheStats::new();
assert_eq!(s.hits(), 0);
assert_eq!(s.misses(), 0);
assert_eq!(s.evictions(), 0);
}
#[test]
fn test_record_hit_increments() {
let mut s = CacheStats::new();
s.record_hit();
s.record_hit();
assert_eq!(s.hits(), 2);
}
#[test]
fn test_record_miss_increments() {
let mut s = CacheStats::new();
s.record_miss();
assert_eq!(s.misses(), 1);
}
#[test]
fn test_record_eviction_increments() {
let mut s = CacheStats::new();
s.record_eviction();
s.record_eviction();
assert_eq!(s.evictions(), 2);
}
#[test]
fn test_hit_rate_zero_when_no_lookups() {
let s = CacheStats::new();
assert!((s.hit_rate() - 0.0).abs() < 1e-6);
}
#[test]
fn test_hit_rate_one_hundred_percent() {
let mut s = CacheStats::new();
s.record_hit();
assert!((s.hit_rate() - 1.0).abs() < 1e-6);
}
#[test]
fn test_hit_rate_two_thirds() {
let mut s = CacheStats::new();
s.record_hit();
s.record_hit();
s.record_miss();
assert!((s.hit_rate() - 2.0 / 3.0).abs() < 1e-5);
}
#[test]
fn test_miss_rate_complement() {
let mut s = CacheStats::new();
s.record_hit();
s.record_miss();
let hit = s.hit_rate();
let miss = s.miss_rate();
assert!((hit + miss - 1.0).abs() < 1e-6);
}
#[test]
fn test_total_lookups_sums_hits_and_misses() {
let mut s = CacheStats::new();
s.record_hit();
s.record_hit();
s.record_miss();
assert_eq!(s.total_lookups(), 3);
}
#[test]
fn test_to_json_contains_hits() {
let mut s = CacheStats::new();
s.record_hit();
s.record_hit();
let json = s.to_json();
assert!(json.contains("\"hits\":2"), "JSON: {json}");
}
#[test]
fn test_to_json_contains_misses() {
let mut s = CacheStats::new();
s.record_miss();
let json = s.to_json();
assert!(json.contains("\"misses\":1"), "JSON: {json}");
}
#[test]
fn test_to_json_contains_evictions() {
let mut s = CacheStats::new();
s.record_eviction();
let json = s.to_json();
assert!(json.contains("\"evictions\":1"), "JSON: {json}");
}
#[test]
fn test_to_json_contains_hit_rate() {
let mut s = CacheStats::new();
s.record_hit();
s.record_miss();
let json = s.to_json();
assert!(json.contains("\"hit_rate\":"), "JSON: {json}");
}
#[test]
fn test_to_json_is_valid_braces() {
let s = CacheStats::new();
let json = s.to_json();
assert!(json.starts_with('{'));
assert!(json.ends_with('}'));
}
#[test]
fn test_reset_clears_all_counters() {
let mut s = CacheStats::new();
s.record_hit();
s.record_miss();
s.record_eviction();
s.reset();
assert_eq!(s.hits(), 0);
assert_eq!(s.misses(), 0);
assert_eq!(s.evictions(), 0);
}
}