1#![allow(dead_code)]
22
23#[derive(Debug, Clone, Default)]
29pub struct CacheStats {
30 hits: u64,
31 misses: u64,
32 evictions: u64,
33}
34
35impl CacheStats {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn record_hit(&mut self) {
44 self.hits = self.hits.saturating_add(1);
45 }
46
47 pub fn record_miss(&mut self) {
49 self.misses = self.misses.saturating_add(1);
50 }
51
52 pub fn record_eviction(&mut self) {
54 self.evictions = self.evictions.saturating_add(1);
55 }
56
57 #[must_use]
59 pub fn hits(&self) -> u64 {
60 self.hits
61 }
62
63 #[must_use]
65 pub fn misses(&self) -> u64 {
66 self.misses
67 }
68
69 #[must_use]
71 pub fn evictions(&self) -> u64 {
72 self.evictions
73 }
74
75 #[must_use]
77 pub fn total_lookups(&self) -> u64 {
78 self.hits.saturating_add(self.misses)
79 }
80
81 #[must_use]
85 pub fn hit_rate(&self) -> f32 {
86 let total = self.total_lookups();
87 if total == 0 {
88 return 0.0;
89 }
90 self.hits as f32 / total as f32
91 }
92
93 #[must_use]
95 pub fn miss_rate(&self) -> f32 {
96 1.0 - self.hit_rate()
97 }
98
99 #[must_use]
107 pub fn to_json(&self) -> String {
108 format!(
109 "{{\"hits\":{},\"misses\":{},\"evictions\":{},\"hit_rate\":{:.6}}}",
110 self.hits,
111 self.misses,
112 self.evictions,
113 self.hit_rate()
114 )
115 }
116
117 pub fn reset(&mut self) {
119 self.hits = 0;
120 self.misses = 0;
121 self.evictions = 0;
122 }
123}
124
125#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
136 fn test_new_starts_at_zero() {
137 let s = CacheStats::new();
138 assert_eq!(s.hits(), 0);
139 assert_eq!(s.misses(), 0);
140 assert_eq!(s.evictions(), 0);
141 }
142
143 #[test]
146 fn test_record_hit_increments() {
147 let mut s = CacheStats::new();
148 s.record_hit();
149 s.record_hit();
150 assert_eq!(s.hits(), 2);
151 }
152
153 #[test]
154 fn test_record_miss_increments() {
155 let mut s = CacheStats::new();
156 s.record_miss();
157 assert_eq!(s.misses(), 1);
158 }
159
160 #[test]
161 fn test_record_eviction_increments() {
162 let mut s = CacheStats::new();
163 s.record_eviction();
164 s.record_eviction();
165 assert_eq!(s.evictions(), 2);
166 }
167
168 #[test]
171 fn test_hit_rate_zero_when_no_lookups() {
172 let s = CacheStats::new();
173 assert!((s.hit_rate() - 0.0).abs() < 1e-6);
174 }
175
176 #[test]
177 fn test_hit_rate_one_hundred_percent() {
178 let mut s = CacheStats::new();
179 s.record_hit();
180 assert!((s.hit_rate() - 1.0).abs() < 1e-6);
181 }
182
183 #[test]
184 fn test_hit_rate_two_thirds() {
185 let mut s = CacheStats::new();
186 s.record_hit();
187 s.record_hit();
188 s.record_miss();
189 assert!((s.hit_rate() - 2.0 / 3.0).abs() < 1e-5);
190 }
191
192 #[test]
193 fn test_miss_rate_complement() {
194 let mut s = CacheStats::new();
195 s.record_hit();
196 s.record_miss();
197 let hit = s.hit_rate();
198 let miss = s.miss_rate();
199 assert!((hit + miss - 1.0).abs() < 1e-6);
200 }
201
202 #[test]
205 fn test_total_lookups_sums_hits_and_misses() {
206 let mut s = CacheStats::new();
207 s.record_hit();
208 s.record_hit();
209 s.record_miss();
210 assert_eq!(s.total_lookups(), 3);
211 }
212
213 #[test]
216 fn test_to_json_contains_hits() {
217 let mut s = CacheStats::new();
218 s.record_hit();
219 s.record_hit();
220 let json = s.to_json();
221 assert!(json.contains("\"hits\":2"), "JSON: {json}");
222 }
223
224 #[test]
225 fn test_to_json_contains_misses() {
226 let mut s = CacheStats::new();
227 s.record_miss();
228 let json = s.to_json();
229 assert!(json.contains("\"misses\":1"), "JSON: {json}");
230 }
231
232 #[test]
233 fn test_to_json_contains_evictions() {
234 let mut s = CacheStats::new();
235 s.record_eviction();
236 let json = s.to_json();
237 assert!(json.contains("\"evictions\":1"), "JSON: {json}");
238 }
239
240 #[test]
241 fn test_to_json_contains_hit_rate() {
242 let mut s = CacheStats::new();
243 s.record_hit();
244 s.record_miss();
245 let json = s.to_json();
246 assert!(json.contains("\"hit_rate\":"), "JSON: {json}");
247 }
248
249 #[test]
250 fn test_to_json_is_valid_braces() {
251 let s = CacheStats::new();
252 let json = s.to_json();
253 assert!(json.starts_with('{'));
254 assert!(json.ends_with('}'));
255 }
256
257 #[test]
260 fn test_reset_clears_all_counters() {
261 let mut s = CacheStats::new();
262 s.record_hit();
263 s.record_miss();
264 s.record_eviction();
265 s.reset();
266 assert_eq!(s.hits(), 0);
267 assert_eq!(s.misses(), 0);
268 assert_eq!(s.evictions(), 0);
269 }
270}