1use std::collections::HashMap;
7
8fn fnv1a_64(data: &[u8]) -> u64 {
12 let mut h: u64 = 14_695_981_039_346_656_037;
13 for &b in data {
14 h ^= b as u64;
15 h = h.wrapping_mul(1_099_511_628_211);
16 }
17 h
18}
19
20fn compute_cid(data: &[u8]) -> String {
22 format!("{:016x}", fnv1a_64(data))
23}
24
25fn now_us() -> u64 {
29 std::time::SystemTime::now()
30 .duration_since(std::time::UNIX_EPOCH)
31 .map(|d| d.as_micros() as u64)
32 .unwrap_or(0)
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct CacheEntry {
40 pub cid: String,
42 pub data: Vec<u8>,
44 pub inserted_at: u64,
46 pub last_accessed: u64,
48 pub access_count: u64,
50 pub ttl_us: Option<u64>,
52 pub size_bytes: usize,
54 pub tags: Vec<String>,
56}
57
58impl CacheEntry {
59 pub fn is_expired(&self, now: u64) -> bool {
61 match self.ttl_us {
62 Some(ttl) => now.saturating_sub(self.inserted_at) >= ttl,
63 None => false,
64 }
65 }
66
67 pub fn remaining_ttl_us(&self, now: u64) -> u64 {
69 match self.ttl_us {
70 Some(ttl) => {
71 let elapsed = now.saturating_sub(self.inserted_at);
72 ttl.saturating_sub(elapsed)
73 }
74 None => u64::MAX,
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub enum EvictionPolicy {
82 Lru,
84 Lfu,
86 TtlFirst,
88 SizeWeighted,
90 Tagged(String),
92}
93
94#[derive(Debug, Clone)]
96pub struct CacheConfig {
97 pub max_entries: usize,
99 pub max_bytes: usize,
101 pub default_ttl_us: Option<u64>,
103 pub policy: EvictionPolicy,
105 pub enable_stats: bool,
107}
108
109impl Default for CacheConfig {
110 fn default() -> Self {
111 Self {
112 max_entries: 1024,
113 max_bytes: 64 * 1024 * 1024, default_ttl_us: None,
115 policy: EvictionPolicy::Lru,
116 enable_stats: true,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Default)]
123pub struct CacheStats {
124 pub hits: u64,
125 pub misses: u64,
126 pub evictions: u64,
127 pub insertions: u64,
128 pub expirations: u64,
129 pub current_entries: usize,
130 pub current_bytes: usize,
131 pub hit_rate: f64,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum CacheError {
138 CidMismatch { expected: String, got: String },
140 EntryTooLarge(usize),
142 CacheAtCapacity,
144 EntryNotFound,
146 TtlExpired,
148}
149
150impl std::fmt::Display for CacheError {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 CacheError::CidMismatch { expected, got } => {
154 write!(f, "CID mismatch: expected {expected}, got {got}")
155 }
156 CacheError::EntryTooLarge(sz) => write!(f, "entry too large: {sz} bytes"),
157 CacheError::CacheAtCapacity => write!(f, "cache at capacity"),
158 CacheError::EntryNotFound => write!(f, "entry not found"),
159 CacheError::TtlExpired => write!(f, "entry TTL expired"),
160 }
161 }
162}
163
164impl std::error::Error for CacheError {}
165
166#[derive(Debug, Clone)]
170pub struct LruNode {
171 pub cid: String,
172 pub prev: Option<usize>,
173 pub next: Option<usize>,
174}
175
176pub struct ContentAddressableCache {
183 config: CacheConfig,
184
185 entries: HashMap<String, CacheEntry>,
187
188 lru_index: HashMap<String, usize>,
190
191 lru_arena: Vec<Option<LruNode>>,
193
194 lru_free: Vec<usize>,
196
197 lru_head: Option<usize>,
199
200 lru_tail: Option<usize>,
202
203 total_bytes: usize,
205
206 hits: u64,
208 misses: u64,
209 evictions: u64,
210 insertions: u64,
211 expirations: u64,
212}
213
214impl ContentAddressableCache {
215 pub fn new(config: CacheConfig) -> Self {
219 let cap = config.max_entries;
220 Self {
221 config,
222 entries: HashMap::with_capacity(cap),
223 lru_index: HashMap::with_capacity(cap),
224 lru_arena: Vec::with_capacity(cap),
225 lru_free: Vec::new(),
226 lru_head: None,
227 lru_tail: None,
228 total_bytes: 0,
229 hits: 0,
230 misses: 0,
231 evictions: 0,
232 insertions: 0,
233 expirations: 0,
234 }
235 }
236
237 pub fn insert(
244 &mut self,
245 data: Vec<u8>,
246 ttl_us: Option<u64>,
247 tags: Vec<String>,
248 ) -> Result<String, CacheError> {
249 let cid = compute_cid(&data);
250 self.insert_with_cid(cid.clone(), data, ttl_us, tags)?;
251 Ok(cid)
252 }
253
254 pub fn insert_with_cid(
259 &mut self,
260 cid: String,
261 data: Vec<u8>,
262 ttl_us: Option<u64>,
263 tags: Vec<String>,
264 ) -> Result<(), CacheError> {
265 let expected = compute_cid(&data);
267 if expected != cid {
268 return Err(CacheError::CidMismatch { expected, got: cid });
269 }
270
271 let size = data.len();
272 if size > self.config.max_bytes {
273 return Err(CacheError::EntryTooLarge(size));
274 }
275
276 if self.entries.contains_key(&cid) {
278 let now = now_us();
279 let entry = self
280 .entries
281 .get_mut(&cid)
282 .ok_or(CacheError::EntryNotFound)?;
283 self.total_bytes -= entry.size_bytes;
284 entry.data = data;
285 entry.inserted_at = now;
286 entry.last_accessed = now;
287 entry.ttl_us = ttl_us.or(self.config.default_ttl_us);
288 entry.tags = tags;
289 entry.size_bytes = size;
290 entry.access_count = 0;
291 self.total_bytes += size;
292 self.lru_touch(&cid.clone());
293 return Ok(());
294 }
295
296 self.expire_ttl_internal();
298 self.evict_to_fit_internal(size)?;
299
300 let now = now_us();
301 let effective_ttl = ttl_us.or(self.config.default_ttl_us);
302 let entry = CacheEntry {
303 cid: cid.clone(),
304 data,
305 inserted_at: now,
306 last_accessed: now,
307 access_count: 0,
308 ttl_us: effective_ttl,
309 size_bytes: size,
310 tags,
311 };
312
313 self.total_bytes += size;
314 self.entries.insert(cid.clone(), entry);
315 self.lru_push_front(&cid);
316
317 if self.config.enable_stats {
318 self.insertions += 1;
319 }
320 Ok(())
321 }
322
323 pub fn get(&mut self, cid: &str) -> Result<&[u8], CacheError> {
329 let now = now_us();
330
331 let expired = self.entries.get(cid).map(|e| e.is_expired(now));
333 match expired {
334 None => {
335 if self.config.enable_stats {
336 self.misses += 1;
337 }
338 return Err(CacheError::EntryNotFound);
339 }
340 Some(true) => {
341 self.remove_internal(cid);
342 if self.config.enable_stats {
343 self.expirations += 1;
344 self.misses += 1;
345 }
346 return Err(CacheError::TtlExpired);
347 }
348 Some(false) => {}
349 }
350
351 if let Some(entry) = self.entries.get_mut(cid) {
353 entry.last_accessed = now;
354 entry.access_count += 1;
355 }
356 self.lru_touch(cid);
357
358 if self.config.enable_stats {
359 self.hits += 1;
360 }
361
362 self.entries
363 .get(cid)
364 .map(|e| e.data.as_slice())
365 .ok_or(CacheError::EntryNotFound)
366 }
367
368 pub fn get_entry(&self, cid: &str) -> Result<&CacheEntry, CacheError> {
371 self.entries.get(cid).ok_or(CacheError::EntryNotFound)
372 }
373
374 pub fn contains(&self, cid: &str) -> bool {
377 match self.entries.get(cid) {
378 Some(e) => !e.is_expired(now_us()),
379 None => false,
380 }
381 }
382
383 pub fn remove(&mut self, cid: &str) -> Result<CacheEntry, CacheError> {
385 match self.remove_internal(cid) {
386 Some(e) => Ok(e),
387 None => Err(CacheError::EntryNotFound),
388 }
389 }
390
391 pub fn remove_by_tag(&mut self, tag: &str) -> Vec<String> {
393 let cids: Vec<String> = self
394 .entries
395 .values()
396 .filter(|e| e.tags.iter().any(|t| t == tag))
397 .map(|e| e.cid.clone())
398 .collect();
399
400 for cid in &cids {
401 self.remove_internal(cid);
402 }
403 cids
404 }
405
406 pub fn expire_ttl(&mut self) -> Vec<String> {
410 self.expire_ttl_internal()
411 }
412
413 pub fn evict_to_fit(&mut self, needed_bytes: usize) -> Vec<String> {
418 let mut evicted = Vec::new();
419 while self.total_bytes + needed_bytes > self.config.max_bytes
420 || self.entries.len() >= self.config.max_entries
421 {
422 if self.entries.is_empty() {
423 break;
424 }
425 if let Some(cid) = self.pick_eviction_candidate() {
426 self.remove_internal(&cid);
427 if self.config.enable_stats {
428 self.evictions += 1;
429 }
430 evicted.push(cid);
431 } else {
432 break;
433 }
434 }
435 evicted
436 }
437
438 pub fn stats(&self) -> CacheStats {
440 let total_lookups = self.hits + self.misses;
441 let hit_rate = if total_lookups > 0 {
442 self.hits as f64 / total_lookups as f64
443 } else {
444 0.0
445 };
446 CacheStats {
447 hits: self.hits,
448 misses: self.misses,
449 evictions: self.evictions,
450 insertions: self.insertions,
451 expirations: self.expirations,
452 current_entries: self.entries.len(),
453 current_bytes: self.total_bytes,
454 hit_rate,
455 }
456 }
457
458 pub fn clear(&mut self) -> usize {
460 let count = self.entries.len();
461 self.entries.clear();
462 self.lru_index.clear();
463 self.lru_arena.clear();
464 self.lru_free.clear();
465 self.lru_head = None;
466 self.lru_tail = None;
467 self.total_bytes = 0;
468 count
469 }
470
471 pub fn cids(&self) -> Vec<String> {
473 self.entries.keys().cloned().collect()
474 }
475
476 fn expire_ttl_internal(&mut self) -> Vec<String> {
479 let now = now_us();
480 let expired: Vec<String> = self
481 .entries
482 .values()
483 .filter(|e| e.is_expired(now))
484 .map(|e| e.cid.clone())
485 .collect();
486
487 for cid in &expired {
488 self.remove_internal(cid);
489 if self.config.enable_stats {
490 self.expirations += 1;
491 }
492 }
493 expired
494 }
495
496 fn evict_to_fit_internal(&mut self, needed_bytes: usize) -> Result<(), CacheError> {
497 let max_attempts = self.entries.len() + 1;
498 let mut attempts = 0;
499
500 while (self.total_bytes + needed_bytes > self.config.max_bytes
501 || self.entries.len() >= self.config.max_entries)
502 && !self.entries.is_empty()
503 {
504 if attempts >= max_attempts {
505 return Err(CacheError::CacheAtCapacity);
506 }
507 attempts += 1;
508
509 if let Some(cid) = self.pick_eviction_candidate() {
510 self.remove_internal(&cid);
511 if self.config.enable_stats {
512 self.evictions += 1;
513 }
514 } else {
515 return Err(CacheError::CacheAtCapacity);
516 }
517 }
518
519 if needed_bytes > self.config.max_bytes {
520 return Err(CacheError::EntryTooLarge(needed_bytes));
521 }
522 Ok(())
523 }
524
525 fn pick_eviction_candidate(&self) -> Option<String> {
527 if self.entries.is_empty() {
528 return None;
529 }
530
531 match &self.config.policy {
532 EvictionPolicy::Lru => {
533 self.lru_tail
535 .and_then(|idx| self.lru_arena.get(idx))
536 .and_then(|node| node.as_ref())
537 .map(|n| n.cid.clone())
538 }
539 EvictionPolicy::Lfu => self
540 .entries
541 .values()
542 .min_by_key(|e| e.access_count)
543 .map(|e| e.cid.clone()),
544 EvictionPolicy::TtlFirst => {
545 let now = now_us();
546 self.entries
547 .values()
548 .min_by_key(|e| e.remaining_ttl_us(now))
549 .map(|e| e.cid.clone())
550 }
551 EvictionPolicy::SizeWeighted => self
552 .entries
553 .values()
554 .max_by_key(|e| e.size_bytes)
555 .map(|e| e.cid.clone()),
556 EvictionPolicy::Tagged(tag) => {
557 let tagged = self
559 .entries
560 .values()
561 .find(|e| e.tags.iter().any(|t| t == tag))
562 .map(|e| e.cid.clone());
563
564 tagged.or_else(|| {
565 self.lru_tail
566 .and_then(|idx| self.lru_arena.get(idx))
567 .and_then(|node| node.as_ref())
568 .map(|n| n.cid.clone())
569 })
570 }
571 }
572 }
573
574 fn remove_internal(&mut self, cid: &str) -> Option<CacheEntry> {
576 let entry = self.entries.remove(cid)?;
577 self.total_bytes -= entry.size_bytes;
578 self.lru_remove(cid);
579 Some(entry)
580 }
581
582 fn lru_alloc(&mut self, node: LruNode) -> usize {
586 if let Some(idx) = self.lru_free.pop() {
587 self.lru_arena[idx] = Some(node);
588 idx
589 } else {
590 let idx = self.lru_arena.len();
591 self.lru_arena.push(Some(node));
592 idx
593 }
594 }
595
596 fn lru_push_front(&mut self, cid: &str) {
598 let node = LruNode {
599 cid: cid.to_owned(),
600 prev: None,
601 next: self.lru_head,
602 };
603 let idx = self.lru_alloc(node);
604
605 if let Some(old_head) = self.lru_head {
606 if let Some(Some(n)) = self.lru_arena.get_mut(old_head) {
607 n.prev = Some(idx);
608 }
609 }
610
611 self.lru_head = Some(idx);
612 if self.lru_tail.is_none() {
613 self.lru_tail = Some(idx);
614 }
615
616 self.lru_index.insert(cid.to_owned(), idx);
617 }
618
619 fn lru_touch(&mut self, cid: &str) {
621 let idx = match self.lru_index.get(cid).copied() {
623 Some(i) => i,
624 None => return,
625 };
626
627 let (prev, next) = {
628 match self.lru_arena.get(idx).and_then(|n| n.as_ref()) {
629 Some(n) => (n.prev, n.next),
630 None => return,
631 }
632 };
633
634 if prev.is_none() {
636 return;
637 }
638
639 if let Some(p) = prev {
641 if let Some(Some(pn)) = self.lru_arena.get_mut(p) {
642 pn.next = next;
643 }
644 }
645 if let Some(nx) = next {
646 if let Some(Some(nn)) = self.lru_arena.get_mut(nx) {
647 nn.prev = prev;
648 }
649 } else {
650 self.lru_tail = prev;
652 }
653
654 if let Some(Some(n)) = self.lru_arena.get_mut(idx) {
656 n.prev = None;
657 n.next = self.lru_head;
658 }
659 if let Some(old_head) = self.lru_head {
660 if let Some(Some(h)) = self.lru_arena.get_mut(old_head) {
661 h.prev = Some(idx);
662 }
663 }
664 self.lru_head = Some(idx);
665 }
666
667 fn lru_remove(&mut self, cid: &str) {
669 let idx = match self.lru_index.remove(cid) {
670 Some(i) => i,
671 None => return,
672 };
673
674 let (prev, next) = {
675 match self.lru_arena.get(idx).and_then(|n| n.as_ref()) {
676 Some(n) => (n.prev, n.next),
677 None => return,
678 }
679 };
680
681 if let Some(p) = prev {
682 if let Some(Some(pn)) = self.lru_arena.get_mut(p) {
683 pn.next = next;
684 }
685 } else {
686 self.lru_head = next;
687 }
688
689 if let Some(nx) = next {
690 if let Some(Some(nn)) = self.lru_arena.get_mut(nx) {
691 nn.prev = prev;
692 }
693 } else {
694 self.lru_tail = prev;
695 }
696
697 self.lru_arena[idx] = None;
698 self.lru_free.push(idx);
699 }
700}
701
702#[cfg(test)]
705mod tests {
706 use super::*;
707
708 struct Xorshift64 {
711 state: u64,
712 }
713
714 impl Xorshift64 {
715 fn new(seed: u64) -> Self {
716 Self {
717 state: if seed == 0 { 1 } else { seed },
718 }
719 }
720
721 fn next(&mut self) -> u64 {
722 self.state ^= self.state << 13;
723 self.state ^= self.state >> 7;
724 self.state ^= self.state << 17;
725 self.state
726 }
727
728 fn bytes(&mut self, len: usize) -> Vec<u8> {
729 let mut out = Vec::with_capacity(len);
730 while out.len() < len {
731 let v = self.next().to_le_bytes();
732 for &b in &v {
733 if out.len() < len {
734 out.push(b);
735 }
736 }
737 }
738 out
739 }
740 }
741
742 fn default_cache() -> ContentAddressableCache {
743 ContentAddressableCache::new(CacheConfig {
744 max_entries: 64,
745 max_bytes: 1024 * 1024,
746 ..Default::default()
747 })
748 }
749
750 #[test]
753 fn test_insert_returns_valid_cid() {
754 let mut c = default_cache();
755 let data = b"hello world".to_vec();
756 let cid = c.insert(data.clone(), None, vec![]).unwrap();
757 assert_eq!(cid.len(), 16);
758 assert_eq!(cid, compute_cid(&data));
759 }
760
761 #[test]
762 fn test_get_returns_correct_data() {
763 let mut c = default_cache();
764 let data = b"test data".to_vec();
765 let cid = c.insert(data.clone(), None, vec![]).unwrap();
766 assert_eq!(c.get(&cid).unwrap(), data.as_slice());
767 }
768
769 #[test]
770 fn test_get_missing_returns_err() {
771 let mut c = default_cache();
772 assert_eq!(c.get("deadbeef00000000"), Err(CacheError::EntryNotFound));
773 }
774
775 #[test]
776 fn test_contains_true_after_insert() {
777 let mut c = default_cache();
778 let cid = c.insert(b"abc".to_vec(), None, vec![]).unwrap();
779 assert!(c.contains(&cid));
780 }
781
782 #[test]
783 fn test_contains_false_for_missing() {
784 let c = default_cache();
785 assert!(!c.contains("0000000000000000"));
786 }
787
788 #[test]
791 fn test_insert_with_cid_success() {
792 let mut c = default_cache();
793 let data = b"verified".to_vec();
794 let cid = compute_cid(&data);
795 c.insert_with_cid(cid.clone(), data, None, vec![]).unwrap();
796 assert!(c.contains(&cid));
797 }
798
799 #[test]
800 fn test_insert_with_cid_mismatch_errors() {
801 let mut c = default_cache();
802 let data = b"real data".to_vec();
803 let bad_cid = "aaaaaaaaaaaaaaaa".to_string();
804 let res = c.insert_with_cid(bad_cid.clone(), data.clone(), None, vec![]);
805 assert!(matches!(
806 res,
807 Err(CacheError::CidMismatch { got, .. }) if got == bad_cid
808 ));
809 }
810
811 #[test]
814 fn test_remove_existing_entry() {
815 let mut c = default_cache();
816 let cid = c.insert(b"remove me".to_vec(), None, vec![]).unwrap();
817 let entry = c.remove(&cid).unwrap();
818 assert_eq!(entry.cid, cid);
819 assert!(!c.contains(&cid));
820 }
821
822 #[test]
823 fn test_remove_missing_returns_err() {
824 let mut c = default_cache();
825 assert_eq!(c.remove("0000000000000001"), Err(CacheError::EntryNotFound));
826 }
827
828 #[test]
831 fn test_get_entry_returns_metadata() {
832 let mut c = default_cache();
833 let data = b"entry meta".to_vec();
834 let cid = c
835 .insert(data.clone(), Some(999_999), vec!["tag1".into()])
836 .unwrap();
837 let entry = c.get_entry(&cid).unwrap();
838 assert_eq!(entry.size_bytes, data.len());
839 assert_eq!(entry.ttl_us, Some(999_999));
840 assert!(entry.tags.contains(&"tag1".to_string()));
841 }
842
843 #[test]
846 fn test_access_count_increments_on_get() {
847 let mut c = default_cache();
848 let cid = c.insert(b"counted".to_vec(), None, vec![]).unwrap();
849 c.get(&cid).unwrap();
850 c.get(&cid).unwrap();
851 assert_eq!(c.get_entry(&cid).unwrap().access_count, 2);
852 }
853
854 #[test]
857 fn test_lru_evicts_least_recently_used() {
858 let mut c = ContentAddressableCache::new(CacheConfig {
860 max_entries: 3,
861 max_bytes: 1024 * 1024,
862 policy: EvictionPolicy::Lru,
863 ..Default::default()
864 });
865
866 let a = c.insert(b"aaaa".to_vec(), None, vec![]).unwrap();
867 let b = c.insert(b"bbbb".to_vec(), None, vec![]).unwrap();
868 let cc = c.insert(b"cccc".to_vec(), None, vec![]).unwrap();
869
870 c.get(&b).unwrap();
874
875 let d = c.insert(b"dddd".to_vec(), None, vec![]).unwrap();
877 assert!(!c.contains(&a), "a should have been evicted");
878 assert!(c.contains(&b));
879 assert!(c.contains(&cc));
880 assert!(c.contains(&d));
881 }
882
883 #[test]
884 fn test_lru_touch_moves_to_front() {
885 let mut c = ContentAddressableCache::new(CacheConfig {
886 max_entries: 3,
887 max_bytes: 1024 * 1024,
888 policy: EvictionPolicy::Lru,
889 ..Default::default()
890 });
891
892 let a = c.insert(b"aaaa".to_vec(), None, vec![]).unwrap();
893 let _b = c.insert(b"bbbb".to_vec(), None, vec![]).unwrap();
894 let _cc = c.insert(b"cccc".to_vec(), None, vec![]).unwrap();
895
896 c.get(&a).unwrap();
898
899 c.insert(b"dddd".to_vec(), None, vec![]).unwrap();
901 assert!(c.contains(&a));
902 }
903
904 #[test]
907 fn test_lfu_evicts_least_frequently_used() {
908 let mut c = ContentAddressableCache::new(CacheConfig {
909 max_entries: 3,
910 max_bytes: 1024 * 1024,
911 policy: EvictionPolicy::Lfu,
912 ..Default::default()
913 });
914
915 let a = c.insert(b"aaaa".to_vec(), None, vec![]).unwrap();
916 let b = c.insert(b"bbbb".to_vec(), None, vec![]).unwrap();
917 let cc = c.insert(b"cccc".to_vec(), None, vec![]).unwrap();
918
919 c.get(&b).unwrap();
921 c.get(&b).unwrap();
922 c.get(&cc).unwrap();
923
924 let d = c.insert(b"dddd".to_vec(), None, vec![]).unwrap();
926 assert!(!c.contains(&a));
927 assert!(c.contains(&b));
928 assert!(c.contains(&cc));
929 assert!(c.contains(&d));
930 }
931
932 #[test]
935 fn test_ttl_expired_entry_removed_on_get() {
936 let mut c = default_cache();
937 let cid = c.insert(b"expire me".to_vec(), Some(1), vec![]).unwrap();
939 std::thread::sleep(std::time::Duration::from_millis(5));
940 assert_eq!(c.get(&cid), Err(CacheError::TtlExpired));
941 assert!(!c.contains(&cid));
942 }
943
944 #[test]
945 fn test_expire_ttl_removes_expired_entries() {
946 let mut c = default_cache();
947 let cid1 = c.insert(b"exp1".to_vec(), Some(10_000), vec![]).unwrap();
949 let cid2 = c.insert(b"keep".to_vec(), None, vec![]).unwrap();
950 std::thread::sleep(std::time::Duration::from_millis(20));
951 let removed = c.expire_ttl();
952 assert!(removed.contains(&cid1));
953 assert!(!removed.contains(&cid2));
954 assert!(!c.contains(&cid1));
955 assert!(c.contains(&cid2));
956 }
957
958 #[test]
959 fn test_contains_returns_false_for_expired() {
960 let mut c = default_cache();
961 let cid = c.insert(b"exp_check".to_vec(), Some(1), vec![]).unwrap();
962 std::thread::sleep(std::time::Duration::from_millis(5));
963 assert!(!c.contains(&cid));
964 }
965
966 #[test]
967 fn test_default_ttl_applied() {
968 let mut c = ContentAddressableCache::new(CacheConfig {
969 default_ttl_us: Some(1),
970 ..Default::default()
971 });
972 let cid = c.insert(b"default_ttl".to_vec(), None, vec![]).unwrap();
973 let entry = c.get_entry(&cid).unwrap();
974 assert_eq!(entry.ttl_us, Some(1));
975 }
976
977 #[test]
980 fn test_ttlfirst_evicts_soonest_expiring() {
981 let mut c = ContentAddressableCache::new(CacheConfig {
982 max_entries: 3,
983 max_bytes: 1024 * 1024,
984 policy: EvictionPolicy::TtlFirst,
985 ..Default::default()
986 });
987
988 let short = c.insert(b"short".to_vec(), Some(10), vec![]).unwrap();
990 let _mid = c
991 .insert(b"middl".to_vec(), Some(100_000_000), vec![])
992 .unwrap();
993 let _long = c.insert(b"longg".to_vec(), None, vec![]).unwrap();
994
995 c.insert(b"ddddd".to_vec(), None, vec![]).unwrap();
997 assert!(!c.contains(&short));
998 }
999
1000 #[test]
1003 fn test_size_weighted_evicts_largest() {
1004 let mut c = ContentAddressableCache::new(CacheConfig {
1005 max_entries: 3,
1006 max_bytes: 1024 * 1024,
1007 policy: EvictionPolicy::SizeWeighted,
1008 ..Default::default()
1009 });
1010
1011 let small = c.insert(b"s".to_vec(), None, vec![]).unwrap();
1012 let large = c.insert(vec![0u8; 512], None, vec![]).unwrap();
1013 let _medium = c.insert(vec![0u8; 128], None, vec![]).unwrap();
1014
1015 c.insert(b"new".to_vec(), None, vec![]).unwrap();
1016 assert!(!c.contains(&large), "largest should be evicted");
1017 assert!(c.contains(&small));
1018 }
1019
1020 #[test]
1023 fn test_tagged_evicts_tagged_entries_first() {
1024 let mut c = ContentAddressableCache::new(CacheConfig {
1025 max_entries: 3,
1026 max_bytes: 1024 * 1024,
1027 policy: EvictionPolicy::Tagged("evict_me".to_string()),
1028 ..Default::default()
1029 });
1030
1031 let tagged = c
1032 .insert(b"tagged_entry".to_vec(), None, vec!["evict_me".into()])
1033 .unwrap();
1034 let _untagged1 = c.insert(b"untagged_a".to_vec(), None, vec![]).unwrap();
1035 let _untagged2 = c.insert(b"untagged_b".to_vec(), None, vec![]).unwrap();
1036
1037 c.insert(b"trigger_evict".to_vec(), None, vec![]).unwrap();
1038 assert!(!c.contains(&tagged));
1039 }
1040
1041 #[test]
1042 fn test_tagged_falls_back_to_lru_when_no_tagged_entry() {
1043 let mut c = ContentAddressableCache::new(CacheConfig {
1044 max_entries: 3,
1045 max_bytes: 1024 * 1024,
1046 policy: EvictionPolicy::Tagged("missing_tag".to_string()),
1047 ..Default::default()
1048 });
1049
1050 let a = c.insert(b"aaaaaa".to_vec(), None, vec![]).unwrap();
1051 let _b = c.insert(b"bbbbbb".to_vec(), None, vec![]).unwrap();
1052 let _cc = c.insert(b"cccccc".to_vec(), None, vec![]).unwrap();
1053
1054 c.insert(b"dddddd".to_vec(), None, vec![]).unwrap();
1055 assert!(!c.contains(&a));
1057 }
1058
1059 #[test]
1062 fn test_remove_by_tag_removes_all_tagged() {
1063 let mut c = default_cache();
1064 let t1 = c
1065 .insert(b"t1data".to_vec(), None, vec!["group".into()])
1066 .unwrap();
1067 let t2 = c
1068 .insert(b"t2data".to_vec(), None, vec!["group".into()])
1069 .unwrap();
1070 let keep = c
1071 .insert(b"keepdt".to_vec(), None, vec!["other".into()])
1072 .unwrap();
1073
1074 let removed = c.remove_by_tag("group");
1075 assert!(removed.contains(&t1));
1076 assert!(removed.contains(&t2));
1077 assert!(!c.contains(&t1));
1078 assert!(!c.contains(&t2));
1079 assert!(c.contains(&keep));
1080 }
1081
1082 #[test]
1083 fn test_remove_by_tag_returns_empty_when_no_match() {
1084 let mut c = default_cache();
1085 c.insert(b"data".to_vec(), None, vec!["other".into()])
1086 .unwrap();
1087 assert!(c.remove_by_tag("nonexistent").is_empty());
1088 }
1089
1090 #[test]
1093 fn test_entry_count_never_exceeds_max() {
1094 let mut c = ContentAddressableCache::new(CacheConfig {
1095 max_entries: 10,
1096 max_bytes: 1024 * 1024,
1097 ..Default::default()
1098 });
1099 let mut rng = Xorshift64::new(42);
1100 for _ in 0..20 {
1101 let data = rng.bytes(16);
1102 let _ = c.insert(data, None, vec![]);
1103 }
1104 assert!(c.stats().current_entries <= 10);
1105 }
1106
1107 #[test]
1108 fn test_byte_limit_never_exceeded() {
1109 let limit = 1024_usize;
1110 let mut c = ContentAddressableCache::new(CacheConfig {
1111 max_entries: 1024,
1112 max_bytes: limit,
1113 ..Default::default()
1114 });
1115 let mut rng = Xorshift64::new(99);
1116 for _ in 0..50 {
1117 let data = rng.bytes(64);
1118 let _ = c.insert(data, None, vec![]);
1119 }
1120 assert!(c.stats().current_bytes <= limit);
1121 }
1122
1123 #[test]
1124 fn test_entry_too_large_rejected() {
1125 let mut c = ContentAddressableCache::new(CacheConfig {
1126 max_entries: 64,
1127 max_bytes: 100,
1128 ..Default::default()
1129 });
1130 let res = c.insert(vec![0u8; 200], None, vec![]);
1131 assert_eq!(res, Err(CacheError::EntryTooLarge(200)));
1132 }
1133
1134 #[test]
1137 fn test_stats_hits_and_misses() {
1138 let mut c = default_cache();
1139 let cid = c.insert(b"stats_test".to_vec(), None, vec![]).unwrap();
1140 c.get(&cid).unwrap();
1141 c.get(&cid).unwrap();
1142 let _ = c.get("000000000000cafe");
1143 let s = c.stats();
1144 assert_eq!(s.hits, 2);
1145 assert_eq!(s.misses, 1);
1146 }
1147
1148 #[test]
1149 fn test_stats_hit_rate() {
1150 let mut c = default_cache();
1151 let cid = c.insert(b"hr".to_vec(), None, vec![]).unwrap();
1152 c.get(&cid).unwrap(); let _ = c.get("nope00000000cafe"); let s = c.stats();
1155 assert!((s.hit_rate - 0.5).abs() < f64::EPSILON);
1156 }
1157
1158 #[test]
1159 fn test_stats_insertions_count() {
1160 let mut c = default_cache();
1161 c.insert(b"a".to_vec(), None, vec![]).unwrap();
1162 c.insert(b"b".to_vec(), None, vec![]).unwrap();
1163 c.insert(b"c".to_vec(), None, vec![]).unwrap();
1164 assert_eq!(c.stats().insertions, 3);
1165 }
1166
1167 #[test]
1168 fn test_stats_eviction_count() {
1169 let mut c = ContentAddressableCache::new(CacheConfig {
1170 max_entries: 2,
1171 max_bytes: 1024 * 1024,
1172 ..Default::default()
1173 });
1174 c.insert(b"first_".to_vec(), None, vec![]).unwrap();
1175 c.insert(b"second".to_vec(), None, vec![]).unwrap();
1176 c.insert(b"third_".to_vec(), None, vec![]).unwrap(); assert!(c.stats().evictions >= 1);
1178 }
1179
1180 #[test]
1181 fn test_stats_current_bytes() {
1182 let mut c = default_cache();
1183 let data = vec![0u8; 128];
1184 c.insert(data, None, vec![]).unwrap();
1185 assert_eq!(c.stats().current_bytes, 128);
1186 }
1187
1188 #[test]
1189 fn test_stats_current_entries() {
1190 let mut c = default_cache();
1191 c.insert(b"e1".to_vec(), None, vec![]).unwrap();
1192 c.insert(b"e2".to_vec(), None, vec![]).unwrap();
1193 assert_eq!(c.stats().current_entries, 2);
1194 }
1195
1196 #[test]
1197 fn test_stats_expirations_count() {
1198 let mut c = default_cache();
1199 c.insert(b"will_expire".to_vec(), Some(1), vec![]).unwrap();
1200 std::thread::sleep(std::time::Duration::from_millis(5));
1201 c.expire_ttl();
1202 assert_eq!(c.stats().expirations, 1);
1203 }
1204
1205 #[test]
1208 fn test_clear_removes_all() {
1209 let mut c = default_cache();
1210 c.insert(b"x".to_vec(), None, vec![]).unwrap();
1211 c.insert(b"y".to_vec(), None, vec![]).unwrap();
1212 let n = c.clear();
1213 assert_eq!(n, 2);
1214 assert_eq!(c.stats().current_entries, 0);
1215 assert_eq!(c.stats().current_bytes, 0);
1216 }
1217
1218 #[test]
1221 fn test_cids_returns_all_current_cids() {
1222 let mut c = default_cache();
1223 let cid1 = c.insert(b"cid_test_1".to_vec(), None, vec![]).unwrap();
1224 let cid2 = c.insert(b"cid_test_2".to_vec(), None, vec![]).unwrap();
1225 let cids = c.cids();
1226 assert!(cids.contains(&cid1));
1227 assert!(cids.contains(&cid2));
1228 assert_eq!(cids.len(), 2);
1229 }
1230
1231 #[test]
1234 fn test_evict_to_fit_makes_room() {
1235 let limit = 256_usize;
1236 let mut c = ContentAddressableCache::new(CacheConfig {
1237 max_entries: 64,
1238 max_bytes: limit,
1239 ..Default::default()
1240 });
1241 let mut rng = Xorshift64::new(77);
1243 for _ in 0..8 {
1244 let data = rng.bytes(32);
1245 c.insert(data, None, vec![]).unwrap();
1246 }
1247 assert!(c.stats().current_bytes <= limit);
1248 let evicted = c.evict_to_fit(32);
1250 assert!(!evicted.is_empty() || c.stats().current_bytes + 32 <= limit);
1251 }
1252
1253 #[test]
1256 fn test_duplicate_insert_updates_entry() {
1257 let mut c = default_cache();
1258 let data = b"same data".to_vec();
1259 let cid = c
1260 .insert(data.clone(), Some(1_000_000), vec!["old".into()])
1261 .unwrap();
1262 c.insert(data.clone(), Some(9_999_999), vec!["new".into()])
1264 .unwrap();
1265 let entry = c.get_entry(&cid).unwrap();
1266 assert_eq!(entry.ttl_us, Some(9_999_999));
1267 assert!(entry.tags.contains(&"new".to_string()));
1268 assert_eq!(c.stats().current_entries, 1);
1270 }
1271
1272 #[test]
1275 fn test_cid_is_deterministic() {
1276 let data = b"deterministic payload";
1277 let c1 = compute_cid(data);
1278 let c2 = compute_cid(data);
1279 assert_eq!(c1, c2);
1280 }
1281
1282 #[test]
1283 fn test_different_data_different_cid() {
1284 let c1 = compute_cid(b"apple");
1285 let c2 = compute_cid(b"orange");
1286 assert_ne!(c1, c2);
1287 }
1288
1289 #[test]
1292 fn test_lru_integrity_many_inserts() {
1293 let mut c = ContentAddressableCache::new(CacheConfig {
1294 max_entries: 8,
1295 max_bytes: 1024 * 1024,
1296 policy: EvictionPolicy::Lru,
1297 ..Default::default()
1298 });
1299 let mut rng = Xorshift64::new(1234);
1300 let mut inserted = Vec::new();
1301 for _ in 0..20 {
1302 let data = rng.bytes(16);
1303 if let Ok(cid) = c.insert(data, None, vec![]) {
1304 inserted.push(cid);
1305 }
1306 }
1307 assert!(c.stats().current_entries <= 8);
1308 }
1309
1310 #[test]
1313 fn test_zero_byte_insert_and_get() {
1314 let mut c = default_cache();
1315 let cid = c.insert(vec![], None, vec![]).unwrap();
1316 assert_eq!(c.get(&cid).unwrap(), &[] as &[u8]);
1317 }
1318
1319 #[test]
1322 fn test_insert_exactly_at_byte_limit() {
1323 let limit = 512;
1324 let mut c = ContentAddressableCache::new(CacheConfig {
1325 max_entries: 64,
1326 max_bytes: limit,
1327 ..Default::default()
1328 });
1329 let data = vec![0u8; limit];
1330 c.insert(data, None, vec![]).unwrap();
1331 assert_eq!(c.stats().current_bytes, limit);
1332 }
1333
1334 #[test]
1337 fn test_entry_can_have_multiple_tags() {
1338 let mut c = default_cache();
1339 let cid = c
1340 .insert(
1341 b"multi_tag".to_vec(),
1342 None,
1343 vec!["a".into(), "b".into(), "c".into()],
1344 )
1345 .unwrap();
1346 let entry = c.get_entry(&cid).unwrap();
1347 assert_eq!(entry.tags.len(), 3);
1348 }
1349
1350 #[test]
1353 fn test_stats_disabled_no_counters() {
1354 let mut c = ContentAddressableCache::new(CacheConfig {
1355 enable_stats: false,
1356 ..Default::default()
1357 });
1358 let cid = c.insert(b"no_stats".to_vec(), None, vec![]).unwrap();
1359 c.get(&cid).unwrap();
1360 let s = c.stats();
1361 assert_eq!(s.hits, 0);
1363 assert_eq!(s.insertions, 0);
1364 }
1365
1366 #[test]
1369 fn test_lru_order_after_remove() {
1370 let mut c = ContentAddressableCache::new(CacheConfig {
1371 max_entries: 4,
1372 max_bytes: 1024 * 1024,
1373 policy: EvictionPolicy::Lru,
1374 ..Default::default()
1375 });
1376
1377 let a = c.insert(b"aaaaa_lru".to_vec(), None, vec![]).unwrap();
1378 let b = c.insert(b"bbbbb_lru".to_vec(), None, vec![]).unwrap();
1379 let cc = c.insert(b"ccccc_lru".to_vec(), None, vec![]).unwrap();
1380
1381 c.remove(&cc).unwrap();
1383 c.remove(&a).unwrap();
1384
1385 c.insert(b"d_lru_test".to_vec(), None, vec![]).unwrap();
1387 c.insert(b"e_lru_test".to_vec(), None, vec![]).unwrap();
1388 c.insert(b"f_lru_test".to_vec(), None, vec![]).unwrap();
1389 c.insert(b"g_lru_test".to_vec(), None, vec![]).unwrap();
1391 assert!(!c.contains(&b));
1392 }
1393
1394 #[test]
1397 fn test_mixed_ttl_and_no_ttl() {
1398 let mut c = default_cache();
1399 let exp = c
1401 .insert(b"expiring_x".to_vec(), Some(10_000), vec![])
1402 .unwrap();
1403 let keep = c.insert(b"permanent_y".to_vec(), None, vec![]).unwrap();
1404 std::thread::sleep(std::time::Duration::from_millis(20));
1405 c.expire_ttl();
1406 assert!(!c.contains(&exp));
1407 assert!(c.contains(&keep));
1408 }
1409
1410 #[test]
1413 fn test_miss_counter_increments() {
1414 let mut c = default_cache();
1415 let _ = c.get("missing_cid_cafe01");
1416 let _ = c.get("missing_cid_cafe02");
1417 assert_eq!(c.stats().misses, 2);
1418 }
1419
1420 #[test]
1423 fn test_expired_get_counts_as_miss() {
1424 let mut c = default_cache();
1425 let cid = c.insert(b"short_live".to_vec(), Some(1), vec![]).unwrap();
1426 std::thread::sleep(std::time::Duration::from_millis(5));
1427 let _ = c.get(&cid);
1428 assert_eq!(c.stats().misses, 1);
1429 }
1430
1431 #[test]
1434 fn test_evict_to_fit_zero_needed() {
1435 let mut c = default_cache();
1436 c.insert(b"zero_fit".to_vec(), None, vec![]).unwrap();
1437 let evicted = c.evict_to_fit(0);
1438 assert!(evicted.is_empty());
1439 }
1440
1441 #[test]
1444 fn test_cid_is_16_hex_chars() {
1445 let mut rng = Xorshift64::new(55);
1446 let mut c = default_cache();
1447 for _ in 0..10 {
1448 let data = rng.bytes(32);
1449 let cid = c.insert(data, None, vec![]).unwrap();
1450 assert_eq!(cid.len(), 16, "CID should be 16 hex chars");
1451 assert!(cid.chars().all(|ch| ch.is_ascii_hexdigit()));
1452 }
1453 }
1454
1455 #[test]
1458 fn test_remove_by_tag_multi_tag_entries() {
1459 let mut c = default_cache();
1460 let cid = c
1461 .insert(
1462 b"multi_tagged_e".to_vec(),
1463 None,
1464 vec!["x".into(), "y".into()],
1465 )
1466 .unwrap();
1467 let removed = c.remove_by_tag("x");
1468 assert!(removed.contains(&cid));
1469 let removed2 = c.remove_by_tag("y");
1470 assert!(removed2.is_empty()); }
1472
1473 #[test]
1476 fn test_clear_resets_byte_count() {
1477 let mut c = default_cache();
1478 c.insert(vec![0u8; 256], None, vec![]).unwrap();
1479 c.clear();
1480 assert_eq!(c.stats().current_bytes, 0);
1481 }
1482
1483 #[test]
1486 fn test_get_entry_missing_returns_err() {
1487 let c = default_cache();
1488 assert_eq!(
1489 c.get_entry("cafecafecafecafe"),
1490 Err(CacheError::EntryNotFound)
1491 );
1492 }
1493
1494 #[test]
1497 fn test_insert_after_clear() {
1498 let mut c = default_cache();
1499 c.insert(b"before".to_vec(), None, vec![]).unwrap();
1500 c.clear();
1501 let cid = c.insert(b"after ".to_vec(), None, vec![]).unwrap();
1502 assert!(c.contains(&cid));
1503 }
1504
1505 #[test]
1508 fn test_lfu_equal_counts_evicts_one() {
1509 let mut c = ContentAddressableCache::new(CacheConfig {
1510 max_entries: 2,
1511 max_bytes: 1024 * 1024,
1512 policy: EvictionPolicy::Lfu,
1513 ..Default::default()
1514 });
1515 c.insert(b"lfu_a".to_vec(), None, vec![]).unwrap();
1516 c.insert(b"lfu_b".to_vec(), None, vec![]).unwrap();
1517 c.insert(b"lfu_c".to_vec(), None, vec![]).unwrap();
1519 assert_eq!(c.stats().current_entries, 2);
1520 }
1521
1522 #[test]
1525 fn test_bytes_freed_on_remove() {
1526 let mut c = default_cache();
1527 let cid = c.insert(vec![0u8; 100], None, vec![]).unwrap();
1528 assert_eq!(c.stats().current_bytes, 100);
1529 c.remove(&cid).unwrap();
1530 assert_eq!(c.stats().current_bytes, 0);
1531 }
1532
1533 #[test]
1536 fn test_bytes_freed_on_expire() {
1537 let mut c = default_cache();
1538 c.insert(vec![0u8; 200], Some(1), vec![]).unwrap();
1539 std::thread::sleep(std::time::Duration::from_millis(5));
1540 c.expire_ttl();
1541 assert_eq!(c.stats().current_bytes, 0);
1542 }
1543
1544 #[test]
1547 fn test_size_weighted_equal_sizes() {
1548 let mut c = ContentAddressableCache::new(CacheConfig {
1549 max_entries: 2,
1550 max_bytes: 1024 * 1024,
1551 policy: EvictionPolicy::SizeWeighted,
1552 ..Default::default()
1553 });
1554 c.insert(b"sw_aa".to_vec(), None, vec![]).unwrap();
1555 c.insert(b"sw_bb".to_vec(), None, vec![]).unwrap();
1556 c.insert(b"sw_cc".to_vec(), None, vec![]).unwrap();
1557 assert_eq!(c.stats().current_entries, 2);
1558 }
1559
1560 #[test]
1563 fn test_byte_limit_exact_fit() {
1564 let limit = 100_usize;
1565 let mut c = ContentAddressableCache::new(CacheConfig {
1566 max_entries: 64,
1567 max_bytes: limit,
1568 ..Default::default()
1569 });
1570 c.insert(vec![1u8; 50], None, vec![]).unwrap();
1572 c.insert(vec![2u8; 50], None, vec![]).unwrap();
1573 assert_eq!(c.stats().current_bytes, 100);
1574 c.insert(vec![3u8; 50], None, vec![]).unwrap();
1576 assert!(c.stats().current_bytes <= 100);
1577 }
1578
1579 #[test]
1582 fn test_last_accessed_updates_on_get() {
1583 let mut c = default_cache();
1584 let cid = c.insert(b"access_ts".to_vec(), None, vec![]).unwrap();
1585 let before = c.get_entry(&cid).unwrap().last_accessed;
1586 std::thread::sleep(std::time::Duration::from_millis(2));
1587 c.get(&cid).unwrap();
1588 let after = c.get_entry(&cid).unwrap().last_accessed;
1589 assert!(after >= before);
1590 }
1591
1592 #[test]
1595 fn test_inserted_at_is_set() {
1596 let mut c = default_cache();
1597 let cid = c.insert(b"timestamp".to_vec(), None, vec![]).unwrap();
1598 let entry = c.get_entry(&cid).unwrap();
1599 assert!(entry.inserted_at > 0);
1600 }
1601
1602 #[test]
1605 fn test_expire_ttl_empty_cache() {
1606 let mut c = default_cache();
1607 let removed = c.expire_ttl();
1608 assert!(removed.is_empty());
1609 }
1610
1611 #[test]
1614 fn test_remove_by_tag_empty_cache() {
1615 let mut c = default_cache();
1616 assert!(c.remove_by_tag("any_tag").is_empty());
1617 }
1618
1619 #[test]
1622 fn test_ttl_first_no_ttl_entries() {
1623 let mut c = ContentAddressableCache::new(CacheConfig {
1624 max_entries: 2,
1625 max_bytes: 1024 * 1024,
1626 policy: EvictionPolicy::TtlFirst,
1627 ..Default::default()
1628 });
1629 c.insert(b"no_ttl_x".to_vec(), None, vec![]).unwrap();
1630 c.insert(b"no_ttl_y".to_vec(), None, vec![]).unwrap();
1631 c.insert(b"no_ttl_z".to_vec(), None, vec![]).unwrap();
1632 assert_eq!(c.stats().current_entries, 2);
1634 }
1635
1636 #[test]
1639 fn test_cache_error_display() {
1640 let e = CacheError::EntryNotFound;
1641 assert!(!e.to_string().is_empty());
1642 let e2 = CacheError::EntryTooLarge(999);
1643 assert!(e2.to_string().contains("999"));
1644 let e3 = CacheError::CidMismatch {
1645 expected: "aaa".into(),
1646 got: "bbb".into(),
1647 };
1648 assert!(e3.to_string().contains("aaa"));
1649 }
1650
1651 #[test]
1654 fn test_stats_hit_rate_zero_when_no_lookups() {
1655 let c = default_cache();
1656 assert_eq!(c.stats().hit_rate, 0.0);
1657 }
1658
1659 #[test]
1662 fn test_lru_arena_slot_reused() {
1663 let mut c = ContentAddressableCache::new(CacheConfig {
1664 max_entries: 4,
1665 max_bytes: 1024 * 1024,
1666 ..Default::default()
1667 });
1668 let a = c.insert(b"arena_a".to_vec(), None, vec![]).unwrap();
1669 c.insert(b"arena_b".to_vec(), None, vec![]).unwrap();
1670 c.remove(&a).unwrap();
1671 assert!(!c.lru_free.is_empty());
1673 c.insert(b"arena_c".to_vec(), None, vec![]).unwrap();
1675 assert!(c.lru_free.is_empty() || c.lru_arena.len() <= 3);
1676 }
1677
1678 #[test]
1681 fn test_stress_random_operations() {
1682 let mut c = ContentAddressableCache::new(CacheConfig {
1683 max_entries: 32,
1684 max_bytes: 4096,
1685 policy: EvictionPolicy::Lru,
1686 ..Default::default()
1687 });
1688 let mut rng = Xorshift64::new(999);
1689 let mut cids: Vec<String> = Vec::new();
1690
1691 for _ in 0..200 {
1692 let op = rng.next() % 4;
1693 match op {
1694 0 => {
1695 let sz = ((rng.next() % 128) + 1) as usize;
1697 let data = rng.bytes(sz);
1698 if let Ok(cid) = c.insert(data, None, vec![]) {
1699 cids.push(cid);
1700 }
1701 }
1702 1 => {
1703 if !cids.is_empty() {
1705 let idx = (rng.next() as usize) % cids.len();
1706 let _ = c.get(&cids[idx].clone());
1707 }
1708 }
1709 2 => {
1710 if !cids.is_empty() {
1712 let idx = (rng.next() as usize) % cids.len();
1713 let cid = cids.swap_remove(idx);
1714 let _ = c.remove(&cid);
1715 }
1716 }
1717 _ => {
1718 c.expire_ttl();
1719 }
1720 }
1721 }
1722 let s = c.stats();
1724 assert!(s.current_entries <= 32);
1725 assert!(s.current_bytes <= 4096);
1726 }
1727
1728 #[test]
1731 fn test_duplicate_cid_single_entry() {
1732 let mut c = default_cache();
1733 let data = b"unique_content".to_vec();
1734 c.insert(data.clone(), None, vec![]).unwrap();
1735 c.insert(data, None, vec![]).unwrap();
1736 assert_eq!(c.stats().current_entries, 1);
1737 }
1738
1739 #[test]
1742 fn test_entry_size_bytes_matches_data_len() {
1743 let mut c = default_cache();
1744 let data = vec![42u8; 77];
1745 let cid = c.insert(data.clone(), None, vec![]).unwrap();
1746 let entry = c.get_entry(&cid).unwrap();
1747 assert_eq!(entry.size_bytes, data.len());
1748 assert_eq!(c.stats().current_bytes, 77);
1749 }
1750
1751 #[test]
1754 fn test_lru_single_entry_head_equals_tail() {
1755 let mut c = default_cache();
1756 c.insert(b"only_one_entry".to_vec(), None, vec![]).unwrap();
1757 assert!(c.lru_head.is_some());
1758 assert!(c.lru_tail.is_some());
1759 assert_eq!(c.lru_head, c.lru_tail);
1760 }
1761
1762 #[test]
1765 fn test_lru_empty_after_clear() {
1766 let mut c = default_cache();
1767 c.insert(b"entry_to_clear".to_vec(), None, vec![]).unwrap();
1768 c.clear();
1769 assert!(c.lru_head.is_none());
1770 assert!(c.lru_tail.is_none());
1771 }
1772
1773 #[test]
1776 fn test_hit_rate_all_hits() {
1777 let mut c = default_cache();
1778 let cid = c.insert(b"all_hits".to_vec(), None, vec![]).unwrap();
1779 c.get(&cid).unwrap();
1780 c.get(&cid).unwrap();
1781 c.get(&cid).unwrap();
1782 assert!((c.stats().hit_rate - 1.0).abs() < f64::EPSILON);
1783 }
1784
1785 #[test]
1788 fn test_many_entries_all_retrievable() {
1789 let mut c = ContentAddressableCache::new(CacheConfig {
1790 max_entries: 512,
1791 max_bytes: 64 * 1024 * 1024,
1792 ..Default::default()
1793 });
1794 let mut rng = Xorshift64::new(7777);
1795 let mut kv: Vec<(String, Vec<u8>)> = Vec::new();
1796 for _ in 0..256 {
1797 let data = rng.bytes(64);
1798 if let Ok(cid) = c.insert(data.clone(), None, vec![]) {
1799 kv.push((cid, data));
1800 }
1801 }
1802 for (cid, data) in &kv {
1803 if c.contains(cid) {
1804 assert_eq!(c.get(cid).unwrap(), data.as_slice());
1805 }
1806 }
1807 }
1808}