1use crate::XoverEntry;
44use std::collections::HashMap;
45
46pub trait HeaderCache {
48 fn put(&mut self, article_number: u64, entry: XoverEntry);
50
51 fn get(&mut self, article_number: &u64) -> Option<&XoverEntry>;
53
54 fn contains(&self, article_number: &u64) -> bool;
56
57 fn remove(&mut self, article_number: &u64) -> Option<XoverEntry>;
59
60 fn clear(&mut self);
62
63 fn len(&self) -> usize;
65
66 #[must_use]
68 fn is_empty(&self) -> bool {
69 self.len() == 0
70 }
71
72 fn capacity(&self) -> usize;
74}
75
76#[derive(Debug, Clone)]
137pub struct LruHeaderCache {
138 max_size: usize,
140 entries: HashMap<u64, XoverEntry>,
142 access_order: HashMap<u64, u64>,
145 access_counter: u64,
147}
148
149impl LruHeaderCache {
150 pub fn new(max_size: usize) -> Self {
170 assert!(max_size > 0, "Cache size must be greater than 0");
171 Self {
172 max_size,
173 entries: HashMap::new(),
174 access_order: HashMap::new(),
175 access_counter: 0,
176 }
177 }
178
179 fn evict_lru(&mut self) -> Option<u64> {
183 if self.entries.is_empty() {
184 return None;
185 }
186
187 let lru_article = self
189 .access_order
190 .iter()
191 .min_by_key(|&(_, &access_count)| access_count)
192 .map(|(&article_number, _)| article_number)?;
193
194 self.entries.remove(&lru_article);
195 self.access_order.remove(&lru_article);
196
197 Some(lru_article)
198 }
199
200 fn touch(&mut self, article_number: &u64) {
202 self.access_counter = self.access_counter.wrapping_add(1);
203 self.access_order
204 .insert(*article_number, self.access_counter);
205 }
206}
207
208impl HeaderCache for LruHeaderCache {
209 fn put(&mut self, article_number: u64, entry: XoverEntry) {
210 if self.entries.len() >= self.max_size && !self.entries.contains_key(&article_number) {
212 self.evict_lru();
213 }
214
215 self.entries.insert(article_number, entry);
217 self.touch(&article_number);
218 }
219
220 fn get(&mut self, article_number: &u64) -> Option<&XoverEntry> {
221 if self.entries.contains_key(article_number) {
222 self.touch(article_number);
223 self.entries.get(article_number)
224 } else {
225 None
226 }
227 }
228
229 fn contains(&self, article_number: &u64) -> bool {
230 self.entries.contains_key(article_number)
231 }
232
233 fn remove(&mut self, article_number: &u64) -> Option<XoverEntry> {
234 self.access_order.remove(article_number);
235 self.entries.remove(article_number)
236 }
237
238 fn clear(&mut self) {
239 self.entries.clear();
240 self.access_order.clear();
241 self.access_counter = 0;
242 }
243
244 fn len(&self) -> usize {
245 self.entries.len()
246 }
247
248 fn capacity(&self) -> usize {
249 self.max_size
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn create_test_entry(article_number: u64, subject: &str) -> XoverEntry {
258 XoverEntry {
259 article_number,
260 subject: subject.to_string(),
261 author: format!("author{}@example.com", article_number),
262 date: format!("2024-01-{:02}", article_number % 31 + 1),
263 message_id: format!("<{}@example.com>", article_number),
264 references: String::new(),
265 bytes: (article_number * 100) as usize,
266 lines: (article_number * 10) as usize,
267 }
268 }
269
270 #[test]
271 fn test_cache_creation() {
272 let cache = LruHeaderCache::new(100);
273 assert_eq!(cache.capacity(), 100);
274 assert_eq!(cache.len(), 0);
275 assert!(cache.is_empty());
276 }
277
278 #[test]
279 #[should_panic(expected = "Cache size must be greater than 0")]
280 fn test_cache_zero_size_panics() {
281 LruHeaderCache::new(0);
282 }
283
284 #[test]
285 fn test_put_and_get() {
286 let mut cache = LruHeaderCache::new(10);
287 let entry = create_test_entry(12345, "Test Article");
288
289 cache.put(12345, entry.clone());
290 assert_eq!(cache.len(), 1);
291 assert!(!cache.is_empty());
292
293 let retrieved = cache.get(&12345).unwrap();
294 assert_eq!(retrieved.article_number, 12345);
295 assert_eq!(retrieved.subject, "Test Article");
296 }
297
298 #[test]
299 fn test_contains() {
300 let mut cache = LruHeaderCache::new(10);
301 let entry = create_test_entry(100, "Test");
302
303 assert!(!cache.contains(&100));
304 cache.put(100, entry);
305 assert!(cache.contains(&100));
306 }
307
308 #[test]
309 fn test_remove() {
310 let mut cache = LruHeaderCache::new(10);
311 let entry = create_test_entry(200, "Remove Me");
312
313 cache.put(200, entry.clone());
314 assert!(cache.contains(&200));
315
316 let removed = cache.remove(&200).unwrap();
317 assert_eq!(removed.article_number, 200);
318 assert!(!cache.contains(&200));
319 assert_eq!(cache.len(), 0);
320 }
321
322 #[test]
323 fn test_clear() {
324 let mut cache = LruHeaderCache::new(10);
325 for i in 1..=5 {
326 cache.put(i, create_test_entry(i, &format!("Article {}", i)));
327 }
328 assert_eq!(cache.len(), 5);
329
330 cache.clear();
331 assert_eq!(cache.len(), 0);
332 assert!(cache.is_empty());
333 for i in 1..=5 {
334 assert!(!cache.contains(&i));
335 }
336 }
337
338 #[test]
339 fn test_lru_eviction() {
340 let mut cache = LruHeaderCache::new(3);
341
342 cache.put(1, create_test_entry(1, "First"));
344 cache.put(2, create_test_entry(2, "Second"));
345 cache.put(3, create_test_entry(3, "Third"));
346 assert_eq!(cache.len(), 3);
347
348 cache.get(&1);
350
351 cache.put(4, create_test_entry(4, "Fourth"));
353 assert_eq!(cache.len(), 3);
354 assert!(cache.contains(&1)); assert!(!cache.contains(&2)); assert!(cache.contains(&3)); assert!(cache.contains(&4)); }
359
360 #[test]
361 fn test_lru_eviction_order() {
362 let mut cache = LruHeaderCache::new(2);
363
364 cache.put(1, create_test_entry(1, "First"));
365 cache.put(2, create_test_entry(2, "Second"));
366
367 cache.get(&1);
369
370 cache.put(3, create_test_entry(3, "Third"));
372 assert!(cache.contains(&1));
373 assert!(!cache.contains(&2));
374 assert!(cache.contains(&3));
375
376 cache.get(&3);
378
379 cache.put(4, create_test_entry(4, "Fourth"));
381 assert!(!cache.contains(&1));
382 assert!(cache.contains(&3));
383 assert!(cache.contains(&4));
384 }
385
386 #[test]
387 fn test_update_existing_entry() {
388 let mut cache = LruHeaderCache::new(3);
389
390 cache.put(1, create_test_entry(1, "Original"));
391 cache.put(2, create_test_entry(2, "Second"));
392 cache.put(3, create_test_entry(3, "Third"));
393
394 cache.put(1, create_test_entry(1, "Updated"));
396 assert_eq!(cache.len(), 3);
397
398 let entry = cache.get(&1).unwrap();
399 assert_eq!(entry.subject, "Updated");
400 }
401
402 #[test]
403 fn test_get_nonexistent() {
404 let mut cache = LruHeaderCache::new(10);
405 assert!(cache.get(&999).is_none());
406 }
407
408 #[test]
409 fn test_remove_nonexistent() {
410 let mut cache = LruHeaderCache::new(10);
411 assert!(cache.remove(&999).is_none());
412 }
413
414 #[test]
415 fn test_large_cache() {
416 let mut cache = LruHeaderCache::new(1000);
417
418 for i in 1..=500 {
420 cache.put(i, create_test_entry(i, &format!("Article {}", i)));
421 }
422 assert_eq!(cache.len(), 500);
423
424 for i in 1..=500 {
426 assert!(cache.contains(&i));
427 let entry = cache.get(&i).unwrap();
428 assert_eq!(entry.article_number, i);
429 }
430 }
431
432 #[test]
433 fn test_single_entry_cache() {
434 let mut cache = LruHeaderCache::new(1);
435
436 cache.put(1, create_test_entry(1, "First"));
437 assert_eq!(cache.len(), 1);
438
439 cache.put(2, create_test_entry(2, "Second"));
441 assert_eq!(cache.len(), 1);
442 assert!(!cache.contains(&1));
443 assert!(cache.contains(&2));
444 }
445
446 #[test]
447 fn test_high_churn() {
448 let mut cache = LruHeaderCache::new(5);
449
450 for i in 1..=100 {
452 cache.put(i, create_test_entry(i, &format!("Article {}", i)));
453 assert!(cache.len() <= 5);
454 }
455
456 assert_eq!(cache.len(), 5);
458 for i in 96..=100 {
459 assert!(cache.contains(&i));
460 }
461 for i in 1..=95 {
462 assert!(!cache.contains(&i));
463 }
464 }
465
466 #[test]
467 fn test_access_pattern_preservation() {
468 let mut cache = LruHeaderCache::new(3);
469
470 cache.put(1, create_test_entry(1, "First"));
471 cache.put(2, create_test_entry(2, "Second"));
472 cache.put(3, create_test_entry(3, "Third"));
473
474 for _ in 0..5 {
476 cache.get(&1);
477 cache.get(&2);
478 }
479
480 cache.put(4, create_test_entry(4, "Fourth"));
482 assert!(cache.contains(&1));
483 assert!(cache.contains(&2));
484 assert!(!cache.contains(&3));
485 assert!(cache.contains(&4));
486
487 cache.put(5, create_test_entry(5, "Fifth"));
488 assert!(cache.contains(&1) || cache.contains(&2)); assert!(!cache.contains(&3));
490 }
491}