1use std::collections::{HashMap, HashSet};
10use std::hash::{Hash, Hasher};
11use std::sync::{Arc, Mutex};
12use std::time::{Duration, Instant};
13
14pub type TripleKey = String;
16
17#[derive(Debug, Clone)]
19pub struct CachedValidationResult {
20 pub focus_node: String,
22 pub shape_id: String,
24 pub is_valid: bool,
26 pub violation_messages: Vec<String>,
28 pub cached_at: Instant,
30 pub ttl: Duration,
32 pub accessed_triples: HashSet<TripleKey>,
36}
37
38impl CachedValidationResult {
39 pub fn new(
41 focus_node: impl Into<String>,
42 shape_id: impl Into<String>,
43 is_valid: bool,
44 violation_messages: Vec<String>,
45 ttl: Duration,
46 ) -> Self {
47 Self {
48 focus_node: focus_node.into(),
49 shape_id: shape_id.into(),
50 is_valid,
51 violation_messages,
52 cached_at: Instant::now(),
53 ttl,
54 accessed_triples: HashSet::new(),
55 }
56 }
57
58 pub fn add_accessed_triple(&mut self, triple: impl Into<TripleKey>) {
60 self.accessed_triples.insert(triple.into());
61 }
62
63 pub fn is_stale(&self) -> bool {
70 self.cached_at.elapsed() >= self.ttl
71 }
72
73 pub fn remaining_ttl(&self) -> Duration {
75 let elapsed = self.cached_at.elapsed();
76 self.ttl.checked_sub(elapsed).unwrap_or(Duration::ZERO)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86pub struct ValidationCacheKey {
87 pub focus_node: String,
89 pub shape_id: String,
91 pub shape_hash: u64,
96}
97
98impl ValidationCacheKey {
99 pub fn new(
101 focus_node: impl Into<String>,
102 shape_id: impl Into<String>,
103 shape_hash: u64,
104 ) -> Self {
105 Self {
106 focus_node: focus_node.into(),
107 shape_id: shape_id.into(),
108 shape_hash,
109 }
110 }
111
112 pub fn hash_shape<T: Hash>(shape: &T) -> u64 {
114 use std::collections::hash_map::DefaultHasher;
115 let mut hasher = DefaultHasher::new();
116 shape.hash(&mut hasher);
117 hasher.finish()
118 }
119}
120
121struct CacheInner {
126 entries: HashMap<ValidationCacheKey, CachedValidationResult>,
127 access_order: HashMap<ValidationCacheKey, usize>,
131 sequence: usize,
133 hit_count: u64,
134 miss_count: u64,
135}
136
137impl CacheInner {
138 fn new() -> Self {
139 Self {
140 entries: HashMap::new(),
141 access_order: HashMap::new(),
142 sequence: 0,
143 hit_count: 0,
144 miss_count: 0,
145 }
146 }
147
148 fn record_access(&mut self, key: &ValidationCacheKey) {
149 self.sequence += 1;
150 self.access_order.insert(key.clone(), self.sequence);
151 }
152
153 fn evict_lru(&mut self) {
155 if let Some((lru_key, _)) = self
156 .access_order
157 .iter()
158 .min_by_key(|(_, &seq)| seq)
159 .map(|(k, v)| (k.clone(), *v))
160 {
161 self.entries.remove(&lru_key);
162 self.access_order.remove(&lru_key);
163 }
164 }
165}
166
167#[derive(Clone)]
192pub struct ValidationCache {
193 inner: Arc<Mutex<CacheInner>>,
194 max_entries: usize,
195 default_ttl: Duration,
196}
197
198impl ValidationCache {
199 pub fn new(max_entries: usize, ttl: Duration) -> Self {
204 Self {
205 inner: Arc::new(Mutex::new(CacheInner::new())),
206 max_entries,
207 default_ttl: ttl,
208 }
209 }
210
211 pub fn get(&self, key: &ValidationCacheKey) -> Option<CachedValidationResult> {
216 let mut inner = self
217 .inner
218 .lock()
219 .expect("cache lock should not be poisoned");
220
221 match inner.entries.get(key) {
222 None => {
223 inner.miss_count += 1;
224 None
225 }
226 Some(entry) if entry.is_stale() => {
227 inner.miss_count += 1;
228 let k = key.clone();
229 inner.entries.remove(&k);
230 inner.access_order.remove(&k);
231 None
232 }
233 Some(_) => {
234 inner.hit_count += 1;
235 inner.record_access(key);
236 inner.entries.get(key).cloned()
237 }
238 }
239 }
240
241 pub fn put(&self, key: ValidationCacheKey, result: CachedValidationResult) {
245 let mut inner = self
246 .inner
247 .lock()
248 .expect("cache lock should not be poisoned");
249
250 if inner.entries.len() >= self.max_entries {
252 let stale_keys: Vec<_> = inner
254 .entries
255 .iter()
256 .filter(|(_, v)| v.is_stale())
257 .map(|(k, _)| k.clone())
258 .collect();
259
260 for sk in stale_keys {
261 inner.entries.remove(&sk);
262 inner.access_order.remove(&sk);
263 }
264
265 if inner.entries.len() >= self.max_entries {
267 inner.evict_lru();
268 }
269 }
270
271 inner.record_access(&key);
272 inner.entries.insert(key, result);
273 }
274
275 pub fn invalidate_node(&self, focus_node: &str) -> usize {
279 let mut inner = self
280 .inner
281 .lock()
282 .expect("cache lock should not be poisoned");
283
284 let to_remove: Vec<_> = inner
285 .entries
286 .keys()
287 .filter(|k| k.focus_node == focus_node)
288 .cloned()
289 .collect();
290
291 let count = to_remove.len();
292 for k in &to_remove {
293 inner.entries.remove(k);
294 inner.access_order.remove(k);
295 }
296 count
297 }
298
299 pub fn invalidate_triple(&self, triple_key: &str) -> usize {
303 let mut inner = self
304 .inner
305 .lock()
306 .expect("cache lock should not be poisoned");
307
308 let to_remove: Vec<_> = inner
309 .entries
310 .iter()
311 .filter(|(_, v)| v.accessed_triples.contains(triple_key))
312 .map(|(k, _)| k.clone())
313 .collect();
314
315 let count = to_remove.len();
316 for k in &to_remove {
317 inner.entries.remove(k);
318 inner.access_order.remove(k);
319 }
320 count
321 }
322
323 pub fn evict_stale(&self) -> usize {
327 let mut inner = self
328 .inner
329 .lock()
330 .expect("cache lock should not be poisoned");
331
332 let stale_keys: Vec<_> = inner
333 .entries
334 .iter()
335 .filter(|(_, v)| v.is_stale())
336 .map(|(k, _)| k.clone())
337 .collect();
338
339 let count = stale_keys.len();
340 for k in &stale_keys {
341 inner.entries.remove(k);
342 inner.access_order.remove(k);
343 }
344 count
345 }
346
347 pub fn clear(&self) {
349 let mut inner = self
350 .inner
351 .lock()
352 .expect("cache lock should not be poisoned");
353 inner.entries.clear();
354 inner.access_order.clear();
355 inner.hit_count = 0;
356 inner.miss_count = 0;
357 inner.sequence = 0;
358 }
359
360 pub fn size(&self) -> usize {
362 let inner = self
363 .inner
364 .lock()
365 .expect("cache lock should not be poisoned");
366 inner.entries.values().filter(|v| !v.is_stale()).count()
367 }
368
369 pub fn raw_size(&self) -> usize {
371 let inner = self
372 .inner
373 .lock()
374 .expect("cache lock should not be poisoned");
375 inner.entries.len()
376 }
377
378 pub fn hit_rate(&self) -> f64 {
380 let inner = self
381 .inner
382 .lock()
383 .expect("cache lock should not be poisoned");
384 let total = inner.hit_count + inner.miss_count;
385 if total == 0 {
386 0.0
387 } else {
388 inner.hit_count as f64 / total as f64
389 }
390 }
391
392 pub fn stats(&self) -> CacheStats {
394 let inner = self
395 .inner
396 .lock()
397 .expect("cache lock should not be poisoned");
398 let total = inner.hit_count + inner.miss_count;
399 CacheStats {
400 entries: inner.entries.len(),
401 hit_count: inner.hit_count,
402 miss_count: inner.miss_count,
403 hit_rate: if total == 0 {
404 0.0
405 } else {
406 inner.hit_count as f64 / total as f64
407 },
408 max_entries: self.max_entries,
409 default_ttl: self.default_ttl,
410 }
411 }
412
413 pub fn default_ttl(&self) -> Duration {
415 self.default_ttl
416 }
417}
418
419#[derive(Debug, Clone)]
421pub struct CacheStats {
422 pub entries: usize,
423 pub hit_count: u64,
424 pub miss_count: u64,
425 pub hit_rate: f64,
426 pub max_entries: usize,
427 pub default_ttl: Duration,
428}
429
430#[cfg(test)]
435mod tests {
436 use super::*;
437 use std::thread;
438
439 fn make_entry(focus: &str, shape: &str, valid: bool, ttl: Duration) -> CachedValidationResult {
440 CachedValidationResult::new(focus, shape, valid, vec![], ttl)
441 }
442
443 fn make_key(focus: &str, shape: &str) -> ValidationCacheKey {
444 ValidationCacheKey::new(focus, shape, 0)
445 }
446
447 #[test]
450 fn test_put_and_get_hit() {
451 let cache = ValidationCache::new(100, Duration::from_secs(60));
452 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
453 let entry = make_entry(
454 "http://ex/Alice",
455 "http://ex/PersonShape",
456 true,
457 Duration::from_secs(60),
458 );
459
460 cache.put(key.clone(), entry);
461 let result = cache.get(&key);
462 assert!(result.is_some());
463 assert!(result.expect("entry should exist").is_valid);
464 }
465
466 #[test]
467 fn test_get_miss() {
468 let cache = ValidationCache::new(100, Duration::from_secs(60));
469 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
470 assert!(cache.get(&key).is_none());
471 }
472
473 #[test]
474 fn test_hit_rate_tracking() {
475 let cache = ValidationCache::new(100, Duration::from_secs(60));
476 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
477 let entry = make_entry(
478 "http://ex/Alice",
479 "http://ex/PersonShape",
480 true,
481 Duration::from_secs(60),
482 );
483
484 cache.put(key.clone(), entry);
485
486 let _ = cache.get(&key);
488 let _ = cache.get(&make_key("http://ex/Bob", "http://ex/PersonShape"));
490
491 let stats = cache.stats();
492 assert_eq!(stats.hit_count, 1);
493 assert_eq!(stats.miss_count, 1);
494 assert!((cache.hit_rate() - 0.5).abs() < f64::EPSILON);
495 }
496
497 #[test]
500 fn test_stale_entry_removed_on_get() {
501 let cache = ValidationCache::new(100, Duration::from_millis(10));
502 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
503 let entry = make_entry(
504 "http://ex/Alice",
505 "http://ex/PersonShape",
506 true,
507 Duration::from_millis(10),
508 );
509
510 cache.put(key.clone(), entry);
511
512 thread::sleep(Duration::from_millis(20));
514
515 let result = cache.get(&key);
516 assert!(result.is_none(), "stale entry should not be returned");
517 }
518
519 #[test]
520 fn test_evict_stale() {
521 let cache = ValidationCache::new(100, Duration::from_millis(10));
522
523 for i in 0..5 {
524 let key = make_key(&format!("http://ex/Node{i}"), "http://ex/S");
525 let entry = make_entry(
526 &format!("http://ex/Node{i}"),
527 "http://ex/S",
528 true,
529 Duration::from_millis(10),
530 );
531 cache.put(key, entry);
532 }
533
534 thread::sleep(Duration::from_millis(20));
535
536 let removed = cache.evict_stale();
537 assert_eq!(removed, 5);
538 assert_eq!(cache.raw_size(), 0);
539 }
540
541 #[test]
544 fn test_invalidate_node() {
545 let cache = ValidationCache::new(100, Duration::from_secs(60));
546
547 cache.put(
548 make_key("http://ex/Alice", "http://ex/S1"),
549 make_entry(
550 "http://ex/Alice",
551 "http://ex/S1",
552 true,
553 Duration::from_secs(60),
554 ),
555 );
556 cache.put(
557 make_key("http://ex/Alice", "http://ex/S2"),
558 make_entry(
559 "http://ex/Alice",
560 "http://ex/S2",
561 true,
562 Duration::from_secs(60),
563 ),
564 );
565 cache.put(
566 make_key("http://ex/Bob", "http://ex/S1"),
567 make_entry(
568 "http://ex/Bob",
569 "http://ex/S1",
570 true,
571 Duration::from_secs(60),
572 ),
573 );
574
575 let removed = cache.invalidate_node("http://ex/Alice");
576 assert_eq!(removed, 2);
577
578 let bob_key = make_key("http://ex/Bob", "http://ex/S1");
580 assert!(cache.get(&bob_key).is_some());
581 }
582
583 #[test]
584 fn test_invalidate_triple() {
585 let cache = ValidationCache::new(100, Duration::from_secs(60));
586
587 let triple = "<http://ex/Alice> <http://ex/age> \"30\"";
588
589 let mut entry_a = make_entry(
590 "http://ex/Alice",
591 "http://ex/S1",
592 true,
593 Duration::from_secs(60),
594 );
595 entry_a.add_accessed_triple(triple);
596
597 let entry_b = make_entry(
598 "http://ex/Bob",
599 "http://ex/S1",
600 true,
601 Duration::from_secs(60),
602 );
603
604 cache.put(make_key("http://ex/Alice", "http://ex/S1"), entry_a);
605 cache.put(make_key("http://ex/Bob", "http://ex/S1"), entry_b);
606
607 let removed = cache.invalidate_triple(triple);
608 assert_eq!(removed, 1);
609
610 assert!(cache
612 .get(&make_key("http://ex/Alice", "http://ex/S1"))
613 .is_none());
614 assert!(cache
615 .get(&make_key("http://ex/Bob", "http://ex/S1"))
616 .is_some());
617 }
618
619 #[test]
622 fn test_lru_eviction_at_capacity() {
623 let cache = ValidationCache::new(3, Duration::from_secs(60));
624
625 for i in 0..3 {
626 cache.put(
627 make_key(&format!("http://ex/Node{i}"), "http://ex/S"),
628 make_entry(
629 &format!("http://ex/Node{i}"),
630 "http://ex/S",
631 true,
632 Duration::from_secs(60),
633 ),
634 );
635 }
636
637 assert_eq!(cache.raw_size(), 3);
638
639 cache.put(
641 make_key("http://ex/Node3", "http://ex/S"),
642 make_entry(
643 "http://ex/Node3",
644 "http://ex/S",
645 true,
646 Duration::from_secs(60),
647 ),
648 );
649
650 assert_eq!(
651 cache.raw_size(),
652 3,
653 "cache should remain at max capacity after LRU eviction"
654 );
655 }
656
657 #[test]
660 fn test_concurrent_put_and_get() {
661 let cache = Arc::new(ValidationCache::new(1000, Duration::from_secs(60)));
662 let mut handles = Vec::new();
663
664 for i in 0..10 {
665 let c = Arc::clone(&cache);
666 handles.push(thread::spawn(move || {
667 let key = make_key(&format!("http://ex/Node{i}"), "http://ex/S");
668 let entry = make_entry(
669 &format!("http://ex/Node{i}"),
670 "http://ex/S",
671 true,
672 Duration::from_secs(60),
673 );
674 c.put(key.clone(), entry);
675 let r = c.get(&key);
676 assert!(r.is_some(), "should find own entry");
677 }));
678 }
679
680 for h in handles {
681 h.join().expect("thread should not panic");
682 }
683 }
684
685 #[test]
688 fn test_clear() {
689 let cache = ValidationCache::new(100, Duration::from_secs(60));
690 cache.put(
691 make_key("http://ex/Alice", "http://ex/S"),
692 make_entry(
693 "http://ex/Alice",
694 "http://ex/S",
695 true,
696 Duration::from_secs(60),
697 ),
698 );
699 cache.clear();
700 assert_eq!(cache.raw_size(), 0);
701 assert_eq!(cache.hit_rate(), 0.0);
702 }
703
704 #[test]
707 fn test_is_stale_false_for_fresh_entry() {
708 let entry = make_entry("http://ex/A", "http://ex/S", true, Duration::from_secs(60));
709 assert!(!entry.is_stale());
710 }
711
712 #[test]
713 fn test_remaining_ttl_nonzero() {
714 let entry = make_entry("http://ex/A", "http://ex/S", true, Duration::from_secs(60));
715 assert!(entry.remaining_ttl() > Duration::ZERO);
716 }
717
718 #[test]
719 fn test_shape_hash_helper() {
720 let h1 = ValidationCacheKey::hash_shape(&"MyShape".to_string());
721 let h2 = ValidationCacheKey::hash_shape(&"MyShape".to_string());
722 assert_eq!(h1, h2);
723
724 let h3 = ValidationCacheKey::hash_shape(&"OtherShape".to_string());
725 assert_ne!(h1, h3);
726 }
727}
728
729#[cfg(test)]
734mod extended_cache_tests {
735 use super::*;
736 use std::thread;
737
738 fn entry(focus: &str, shape: &str, valid: bool) -> CachedValidationResult {
739 CachedValidationResult::new(focus, shape, valid, vec![], Duration::from_secs(60))
740 }
741
742 fn key(focus: &str, shape: &str) -> ValidationCacheKey {
743 ValidationCacheKey::new(focus, shape, 0)
744 }
745
746 #[test]
749 fn test_invalidate_node_removes_single_entry() {
750 let cache = ValidationCache::new(100, Duration::from_secs(60));
751 cache.put(
752 key("http://ex/Alice", "http://ex/S"),
753 entry("http://ex/Alice", "http://ex/S", true),
754 );
755 assert_eq!(cache.raw_size(), 1);
756
757 let removed = cache.invalidate_node("http://ex/Alice");
758 assert_eq!(removed, 1);
759 assert_eq!(cache.raw_size(), 0);
760 }
761
762 #[test]
763 fn test_invalidate_node_removes_multiple_shapes() {
764 let cache = ValidationCache::new(100, Duration::from_secs(60));
765 cache.put(
766 key("http://ex/Alice", "http://ex/S1"),
767 entry("http://ex/Alice", "http://ex/S1", true),
768 );
769 cache.put(
770 key("http://ex/Alice", "http://ex/S2"),
771 entry("http://ex/Alice", "http://ex/S2", false),
772 );
773 cache.put(
774 key("http://ex/Bob", "http://ex/S1"),
775 entry("http://ex/Bob", "http://ex/S1", true),
776 );
777
778 let removed = cache.invalidate_node("http://ex/Alice");
779 assert_eq!(removed, 2, "both Alice entries should be removed");
780 assert_eq!(cache.raw_size(), 1, "Bob entry should remain");
781 }
782
783 #[test]
784 fn test_invalidate_node_nonexistent_is_zero() {
785 let cache = ValidationCache::new(100, Duration::from_secs(60));
786 let removed = cache.invalidate_node("http://ex/NoSuchNode");
787 assert_eq!(removed, 0);
788 }
789
790 #[test]
793 fn test_invalidate_triple_removes_dependent_entries() {
794 let cache = ValidationCache::new(100, Duration::from_secs(60));
795 let triple_key = "http://ex/Alice/name/Bob";
796
797 let mut e = entry("http://ex/Alice", "http://ex/S", true);
798 e.add_accessed_triple(triple_key);
799
800 cache.put(key("http://ex/Alice", "http://ex/S"), e);
801 assert_eq!(cache.raw_size(), 1);
802
803 let removed = cache.invalidate_triple(triple_key);
804 assert_eq!(removed, 1);
805 assert_eq!(cache.raw_size(), 0);
806 }
807
808 #[test]
809 fn test_invalidate_triple_non_dependent_entry_stays() {
810 let cache = ValidationCache::new(100, Duration::from_secs(60));
811 cache.put(
813 key("http://ex/Bob", "http://ex/S"),
814 entry("http://ex/Bob", "http://ex/S", true),
815 );
816
817 let removed = cache.invalidate_triple("some:triple:key");
818 assert_eq!(removed, 0);
819 assert_eq!(cache.raw_size(), 1);
820 }
821
822 #[test]
825 fn test_evict_stale_removes_expired_entries() {
826 let cache = ValidationCache::new(100, Duration::from_secs(60));
827
828 let stale = CachedValidationResult::new(
830 "http://ex/Alice",
831 "http://ex/S",
832 true,
833 vec![],
834 Duration::ZERO, );
836 cache.put(key("http://ex/Alice", "http://ex/S"), stale);
837
838 cache.put(
840 key("http://ex/Bob", "http://ex/S"),
841 entry("http://ex/Bob", "http://ex/S", true),
842 );
843
844 let evicted = cache.evict_stale();
845 assert_eq!(evicted, 1, "one stale entry should be evicted");
846 }
847
848 #[test]
851 fn test_size_excludes_stale_entries() {
852 let cache = ValidationCache::new(100, Duration::from_secs(60));
853
854 let stale = CachedValidationResult::new(
855 "http://ex/Alice",
856 "http://ex/S",
857 true,
858 vec![],
859 Duration::ZERO,
860 );
861 cache.put(key("http://ex/Alice", "http://ex/S"), stale);
862 cache.put(
863 key("http://ex/Bob", "http://ex/S"),
864 entry("http://ex/Bob", "http://ex/S", true),
865 );
866
867 assert_eq!(cache.raw_size(), 2);
869 assert!(cache.size() <= 2);
871 }
872
873 #[test]
876 fn test_entry_is_stale_with_zero_ttl() {
877 let stale = CachedValidationResult::new(
878 "http://ex/Alice",
879 "http://ex/S",
880 true,
881 vec![],
882 Duration::ZERO,
883 );
884 assert!(stale.is_stale());
885 }
886
887 #[test]
888 fn test_entry_not_stale_with_large_ttl() {
889 let fresh = entry("http://ex/Alice", "http://ex/S", true);
890 assert!(!fresh.is_stale());
891 }
892
893 #[test]
894 fn test_remaining_ttl_is_zero_for_stale_entry() {
895 let stale = CachedValidationResult::new(
896 "http://ex/Alice",
897 "http://ex/S",
898 true,
899 vec![],
900 Duration::ZERO,
901 );
902 assert_eq!(stale.remaining_ttl(), Duration::ZERO);
903 }
904
905 #[test]
906 fn test_accessed_triples_recorded() {
907 let mut e = entry("http://ex/Alice", "http://ex/S", true);
908 e.add_accessed_triple("triple:a");
909 e.add_accessed_triple("triple:b");
910 assert_eq!(e.accessed_triples.len(), 2);
911 }
912
913 #[test]
916 fn test_cache_key_equality_same_inputs() {
917 let k1 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S", 0u64);
918 let k2 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S", 0u64);
919 assert_eq!(k1, k2);
920 }
921
922 #[test]
923 fn test_cache_key_inequality_different_shape() {
924 let k1 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S1", 0u64);
925 let k2 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S2", 0u64);
926 assert_ne!(k1, k2);
927 }
928
929 #[test]
930 fn test_cache_key_inequality_different_node() {
931 let k1 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S", 0u64);
932 let k2 = ValidationCacheKey::new("http://ex/Bob", "http://ex/S", 0u64);
933 assert_ne!(k1, k2);
934 }
935
936 #[test]
939 fn test_stats_initial_zero() {
940 let cache = ValidationCache::new(100, Duration::from_secs(60));
941 let stats = cache.stats();
942 assert_eq!(stats.hit_count, 0);
943 assert_eq!(stats.miss_count, 0);
944 assert_eq!(stats.entries, 0);
945 }
946
947 #[test]
948 fn test_stats_put_count_increments() {
949 let cache = ValidationCache::new(100, Duration::from_secs(60));
950 cache.put(
951 key("http://ex/A", "http://ex/S"),
952 entry("http://ex/A", "http://ex/S", true),
953 );
954 cache.put(
955 key("http://ex/B", "http://ex/S"),
956 entry("http://ex/B", "http://ex/S", true),
957 );
958 let stats = cache.stats();
959 assert_eq!(stats.entries, 2);
960 }
961
962 #[test]
963 fn test_stats_miss_increments_on_absent_key() {
964 let cache = ValidationCache::new(100, Duration::from_secs(60));
965 let _ = cache.get(&key("http://ex/NoNode", "http://ex/S"));
966 let stats = cache.stats();
967 assert_eq!(stats.miss_count, 1);
968 }
969
970 #[test]
973 fn test_default_ttl_matches_constructor() {
974 let ttl = Duration::from_secs(120);
975 let cache = ValidationCache::new(50, ttl);
976 assert_eq!(cache.default_ttl(), ttl);
977 }
978
979 #[test]
982 fn test_concurrent_invalidation_safety() {
983 let cache = std::sync::Arc::new(ValidationCache::new(1000, Duration::from_secs(60)));
984
985 for i in 0..50 {
987 cache.put(
988 key(&format!("http://ex/Node{i}"), "http://ex/S"),
989 entry(&format!("http://ex/Node{i}"), "http://ex/S", true),
990 );
991 }
992
993 let cache2 = std::sync::Arc::clone(&cache);
994 let handle = thread::spawn(move || {
995 for i in 0..50 {
996 cache2.invalidate_node(&format!("http://ex/Node{i}"));
997 }
998 });
999
1000 for i in 0..50 {
1002 let _ = cache.get(&key(&format!("http://ex/Node{i}"), "http://ex/S"));
1003 }
1004
1005 handle.join().expect("thread should not panic");
1006 }
1007
1008 #[test]
1011 fn test_hit_rate_zero_when_no_accesses() {
1012 let cache = ValidationCache::new(100, Duration::from_secs(60));
1013 assert_eq!(cache.hit_rate(), 0.0);
1014 }
1015
1016 #[test]
1017 fn test_hit_rate_one_when_all_hits() {
1018 let cache = ValidationCache::new(100, Duration::from_secs(60));
1019 let k = key("http://ex/Alice", "http://ex/S");
1020 cache.put(k.clone(), entry("http://ex/Alice", "http://ex/S", true));
1021 let _ = cache.get(&k);
1022 let _ = cache.get(&k);
1023 assert!((cache.hit_rate() - 1.0).abs() < 1e-9);
1025 }
1026}