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 {
65 self.cached_at.elapsed() > self.ttl
66 }
67
68 pub fn remaining_ttl(&self) -> Duration {
70 let elapsed = self.cached_at.elapsed();
71 self.ttl.checked_sub(elapsed).unwrap_or(Duration::ZERO)
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub struct ValidationCacheKey {
82 pub focus_node: String,
84 pub shape_id: String,
86 pub shape_hash: u64,
91}
92
93impl ValidationCacheKey {
94 pub fn new(
96 focus_node: impl Into<String>,
97 shape_id: impl Into<String>,
98 shape_hash: u64,
99 ) -> Self {
100 Self {
101 focus_node: focus_node.into(),
102 shape_id: shape_id.into(),
103 shape_hash,
104 }
105 }
106
107 pub fn hash_shape<T: Hash>(shape: &T) -> u64 {
109 use std::collections::hash_map::DefaultHasher;
110 let mut hasher = DefaultHasher::new();
111 shape.hash(&mut hasher);
112 hasher.finish()
113 }
114}
115
116struct CacheInner {
121 entries: HashMap<ValidationCacheKey, CachedValidationResult>,
122 access_order: HashMap<ValidationCacheKey, usize>,
126 sequence: usize,
128 hit_count: u64,
129 miss_count: u64,
130}
131
132impl CacheInner {
133 fn new() -> Self {
134 Self {
135 entries: HashMap::new(),
136 access_order: HashMap::new(),
137 sequence: 0,
138 hit_count: 0,
139 miss_count: 0,
140 }
141 }
142
143 fn record_access(&mut self, key: &ValidationCacheKey) {
144 self.sequence += 1;
145 self.access_order.insert(key.clone(), self.sequence);
146 }
147
148 fn evict_lru(&mut self) {
150 if let Some((lru_key, _)) = self
151 .access_order
152 .iter()
153 .min_by_key(|(_, &seq)| seq)
154 .map(|(k, v)| (k.clone(), *v))
155 {
156 self.entries.remove(&lru_key);
157 self.access_order.remove(&lru_key);
158 }
159 }
160}
161
162#[derive(Clone)]
187pub struct ValidationCache {
188 inner: Arc<Mutex<CacheInner>>,
189 max_entries: usize,
190 default_ttl: Duration,
191}
192
193impl ValidationCache {
194 pub fn new(max_entries: usize, ttl: Duration) -> Self {
199 Self {
200 inner: Arc::new(Mutex::new(CacheInner::new())),
201 max_entries,
202 default_ttl: ttl,
203 }
204 }
205
206 pub fn get(&self, key: &ValidationCacheKey) -> Option<CachedValidationResult> {
211 let mut inner = self
212 .inner
213 .lock()
214 .expect("cache lock should not be poisoned");
215
216 match inner.entries.get(key) {
217 None => {
218 inner.miss_count += 1;
219 None
220 }
221 Some(entry) if entry.is_stale() => {
222 inner.miss_count += 1;
223 let k = key.clone();
224 inner.entries.remove(&k);
225 inner.access_order.remove(&k);
226 None
227 }
228 Some(_) => {
229 inner.hit_count += 1;
230 inner.record_access(key);
231 inner.entries.get(key).cloned()
232 }
233 }
234 }
235
236 pub fn put(&self, key: ValidationCacheKey, result: CachedValidationResult) {
240 let mut inner = self
241 .inner
242 .lock()
243 .expect("cache lock should not be poisoned");
244
245 if inner.entries.len() >= self.max_entries {
247 let stale_keys: Vec<_> = inner
249 .entries
250 .iter()
251 .filter(|(_, v)| v.is_stale())
252 .map(|(k, _)| k.clone())
253 .collect();
254
255 for sk in stale_keys {
256 inner.entries.remove(&sk);
257 inner.access_order.remove(&sk);
258 }
259
260 if inner.entries.len() >= self.max_entries {
262 inner.evict_lru();
263 }
264 }
265
266 inner.record_access(&key);
267 inner.entries.insert(key, result);
268 }
269
270 pub fn invalidate_node(&self, focus_node: &str) -> usize {
274 let mut inner = self
275 .inner
276 .lock()
277 .expect("cache lock should not be poisoned");
278
279 let to_remove: Vec<_> = inner
280 .entries
281 .keys()
282 .filter(|k| k.focus_node == focus_node)
283 .cloned()
284 .collect();
285
286 let count = to_remove.len();
287 for k in &to_remove {
288 inner.entries.remove(k);
289 inner.access_order.remove(k);
290 }
291 count
292 }
293
294 pub fn invalidate_triple(&self, triple_key: &str) -> usize {
298 let mut inner = self
299 .inner
300 .lock()
301 .expect("cache lock should not be poisoned");
302
303 let to_remove: Vec<_> = inner
304 .entries
305 .iter()
306 .filter(|(_, v)| v.accessed_triples.contains(triple_key))
307 .map(|(k, _)| k.clone())
308 .collect();
309
310 let count = to_remove.len();
311 for k in &to_remove {
312 inner.entries.remove(k);
313 inner.access_order.remove(k);
314 }
315 count
316 }
317
318 pub fn evict_stale(&self) -> usize {
322 let mut inner = self
323 .inner
324 .lock()
325 .expect("cache lock should not be poisoned");
326
327 let stale_keys: Vec<_> = inner
328 .entries
329 .iter()
330 .filter(|(_, v)| v.is_stale())
331 .map(|(k, _)| k.clone())
332 .collect();
333
334 let count = stale_keys.len();
335 for k in &stale_keys {
336 inner.entries.remove(k);
337 inner.access_order.remove(k);
338 }
339 count
340 }
341
342 pub fn clear(&self) {
344 let mut inner = self
345 .inner
346 .lock()
347 .expect("cache lock should not be poisoned");
348 inner.entries.clear();
349 inner.access_order.clear();
350 inner.hit_count = 0;
351 inner.miss_count = 0;
352 inner.sequence = 0;
353 }
354
355 pub fn size(&self) -> usize {
357 let inner = self
358 .inner
359 .lock()
360 .expect("cache lock should not be poisoned");
361 inner.entries.values().filter(|v| !v.is_stale()).count()
362 }
363
364 pub fn raw_size(&self) -> usize {
366 let inner = self
367 .inner
368 .lock()
369 .expect("cache lock should not be poisoned");
370 inner.entries.len()
371 }
372
373 pub fn hit_rate(&self) -> f64 {
375 let inner = self
376 .inner
377 .lock()
378 .expect("cache lock should not be poisoned");
379 let total = inner.hit_count + inner.miss_count;
380 if total == 0 {
381 0.0
382 } else {
383 inner.hit_count as f64 / total as f64
384 }
385 }
386
387 pub fn stats(&self) -> CacheStats {
389 let inner = self
390 .inner
391 .lock()
392 .expect("cache lock should not be poisoned");
393 let total = inner.hit_count + inner.miss_count;
394 CacheStats {
395 entries: inner.entries.len(),
396 hit_count: inner.hit_count,
397 miss_count: inner.miss_count,
398 hit_rate: if total == 0 {
399 0.0
400 } else {
401 inner.hit_count as f64 / total as f64
402 },
403 max_entries: self.max_entries,
404 default_ttl: self.default_ttl,
405 }
406 }
407
408 pub fn default_ttl(&self) -> Duration {
410 self.default_ttl
411 }
412}
413
414#[derive(Debug, Clone)]
416pub struct CacheStats {
417 pub entries: usize,
418 pub hit_count: u64,
419 pub miss_count: u64,
420 pub hit_rate: f64,
421 pub max_entries: usize,
422 pub default_ttl: Duration,
423}
424
425#[cfg(test)]
430mod tests {
431 use super::*;
432 use std::thread;
433
434 fn make_entry(focus: &str, shape: &str, valid: bool, ttl: Duration) -> CachedValidationResult {
435 CachedValidationResult::new(focus, shape, valid, vec![], ttl)
436 }
437
438 fn make_key(focus: &str, shape: &str) -> ValidationCacheKey {
439 ValidationCacheKey::new(focus, shape, 0)
440 }
441
442 #[test]
445 fn test_put_and_get_hit() {
446 let cache = ValidationCache::new(100, Duration::from_secs(60));
447 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
448 let entry = make_entry(
449 "http://ex/Alice",
450 "http://ex/PersonShape",
451 true,
452 Duration::from_secs(60),
453 );
454
455 cache.put(key.clone(), entry);
456 let result = cache.get(&key);
457 assert!(result.is_some());
458 assert!(result.expect("entry should exist").is_valid);
459 }
460
461 #[test]
462 fn test_get_miss() {
463 let cache = ValidationCache::new(100, Duration::from_secs(60));
464 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
465 assert!(cache.get(&key).is_none());
466 }
467
468 #[test]
469 fn test_hit_rate_tracking() {
470 let cache = ValidationCache::new(100, Duration::from_secs(60));
471 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
472 let entry = make_entry(
473 "http://ex/Alice",
474 "http://ex/PersonShape",
475 true,
476 Duration::from_secs(60),
477 );
478
479 cache.put(key.clone(), entry);
480
481 let _ = cache.get(&key);
483 let _ = cache.get(&make_key("http://ex/Bob", "http://ex/PersonShape"));
485
486 let stats = cache.stats();
487 assert_eq!(stats.hit_count, 1);
488 assert_eq!(stats.miss_count, 1);
489 assert!((cache.hit_rate() - 0.5).abs() < f64::EPSILON);
490 }
491
492 #[test]
495 fn test_stale_entry_removed_on_get() {
496 let cache = ValidationCache::new(100, Duration::from_millis(10));
497 let key = make_key("http://ex/Alice", "http://ex/PersonShape");
498 let entry = make_entry(
499 "http://ex/Alice",
500 "http://ex/PersonShape",
501 true,
502 Duration::from_millis(10),
503 );
504
505 cache.put(key.clone(), entry);
506
507 thread::sleep(Duration::from_millis(20));
509
510 let result = cache.get(&key);
511 assert!(result.is_none(), "stale entry should not be returned");
512 }
513
514 #[test]
515 fn test_evict_stale() {
516 let cache = ValidationCache::new(100, Duration::from_millis(10));
517
518 for i in 0..5 {
519 let key = make_key(&format!("http://ex/Node{i}"), "http://ex/S");
520 let entry = make_entry(
521 &format!("http://ex/Node{i}"),
522 "http://ex/S",
523 true,
524 Duration::from_millis(10),
525 );
526 cache.put(key, entry);
527 }
528
529 thread::sleep(Duration::from_millis(20));
530
531 let removed = cache.evict_stale();
532 assert_eq!(removed, 5);
533 assert_eq!(cache.raw_size(), 0);
534 }
535
536 #[test]
539 fn test_invalidate_node() {
540 let cache = ValidationCache::new(100, Duration::from_secs(60));
541
542 cache.put(
543 make_key("http://ex/Alice", "http://ex/S1"),
544 make_entry(
545 "http://ex/Alice",
546 "http://ex/S1",
547 true,
548 Duration::from_secs(60),
549 ),
550 );
551 cache.put(
552 make_key("http://ex/Alice", "http://ex/S2"),
553 make_entry(
554 "http://ex/Alice",
555 "http://ex/S2",
556 true,
557 Duration::from_secs(60),
558 ),
559 );
560 cache.put(
561 make_key("http://ex/Bob", "http://ex/S1"),
562 make_entry(
563 "http://ex/Bob",
564 "http://ex/S1",
565 true,
566 Duration::from_secs(60),
567 ),
568 );
569
570 let removed = cache.invalidate_node("http://ex/Alice");
571 assert_eq!(removed, 2);
572
573 let bob_key = make_key("http://ex/Bob", "http://ex/S1");
575 assert!(cache.get(&bob_key).is_some());
576 }
577
578 #[test]
579 fn test_invalidate_triple() {
580 let cache = ValidationCache::new(100, Duration::from_secs(60));
581
582 let triple = "<http://ex/Alice> <http://ex/age> \"30\"";
583
584 let mut entry_a = make_entry(
585 "http://ex/Alice",
586 "http://ex/S1",
587 true,
588 Duration::from_secs(60),
589 );
590 entry_a.add_accessed_triple(triple);
591
592 let entry_b = make_entry(
593 "http://ex/Bob",
594 "http://ex/S1",
595 true,
596 Duration::from_secs(60),
597 );
598
599 cache.put(make_key("http://ex/Alice", "http://ex/S1"), entry_a);
600 cache.put(make_key("http://ex/Bob", "http://ex/S1"), entry_b);
601
602 let removed = cache.invalidate_triple(triple);
603 assert_eq!(removed, 1);
604
605 assert!(cache
607 .get(&make_key("http://ex/Alice", "http://ex/S1"))
608 .is_none());
609 assert!(cache
610 .get(&make_key("http://ex/Bob", "http://ex/S1"))
611 .is_some());
612 }
613
614 #[test]
617 fn test_lru_eviction_at_capacity() {
618 let cache = ValidationCache::new(3, Duration::from_secs(60));
619
620 for i in 0..3 {
621 cache.put(
622 make_key(&format!("http://ex/Node{i}"), "http://ex/S"),
623 make_entry(
624 &format!("http://ex/Node{i}"),
625 "http://ex/S",
626 true,
627 Duration::from_secs(60),
628 ),
629 );
630 }
631
632 assert_eq!(cache.raw_size(), 3);
633
634 cache.put(
636 make_key("http://ex/Node3", "http://ex/S"),
637 make_entry(
638 "http://ex/Node3",
639 "http://ex/S",
640 true,
641 Duration::from_secs(60),
642 ),
643 );
644
645 assert_eq!(
646 cache.raw_size(),
647 3,
648 "cache should remain at max capacity after LRU eviction"
649 );
650 }
651
652 #[test]
655 fn test_concurrent_put_and_get() {
656 let cache = Arc::new(ValidationCache::new(1000, Duration::from_secs(60)));
657 let mut handles = Vec::new();
658
659 for i in 0..10 {
660 let c = Arc::clone(&cache);
661 handles.push(thread::spawn(move || {
662 let key = make_key(&format!("http://ex/Node{i}"), "http://ex/S");
663 let entry = make_entry(
664 &format!("http://ex/Node{i}"),
665 "http://ex/S",
666 true,
667 Duration::from_secs(60),
668 );
669 c.put(key.clone(), entry);
670 let r = c.get(&key);
671 assert!(r.is_some(), "should find own entry");
672 }));
673 }
674
675 for h in handles {
676 h.join().expect("thread should not panic");
677 }
678 }
679
680 #[test]
683 fn test_clear() {
684 let cache = ValidationCache::new(100, Duration::from_secs(60));
685 cache.put(
686 make_key("http://ex/Alice", "http://ex/S"),
687 make_entry(
688 "http://ex/Alice",
689 "http://ex/S",
690 true,
691 Duration::from_secs(60),
692 ),
693 );
694 cache.clear();
695 assert_eq!(cache.raw_size(), 0);
696 assert_eq!(cache.hit_rate(), 0.0);
697 }
698
699 #[test]
702 fn test_is_stale_false_for_fresh_entry() {
703 let entry = make_entry("http://ex/A", "http://ex/S", true, Duration::from_secs(60));
704 assert!(!entry.is_stale());
705 }
706
707 #[test]
708 fn test_remaining_ttl_nonzero() {
709 let entry = make_entry("http://ex/A", "http://ex/S", true, Duration::from_secs(60));
710 assert!(entry.remaining_ttl() > Duration::ZERO);
711 }
712
713 #[test]
714 fn test_shape_hash_helper() {
715 let h1 = ValidationCacheKey::hash_shape(&"MyShape".to_string());
716 let h2 = ValidationCacheKey::hash_shape(&"MyShape".to_string());
717 assert_eq!(h1, h2);
718
719 let h3 = ValidationCacheKey::hash_shape(&"OtherShape".to_string());
720 assert_ne!(h1, h3);
721 }
722}
723
724#[cfg(test)]
729mod extended_cache_tests {
730 use super::*;
731 use std::thread;
732
733 fn entry(focus: &str, shape: &str, valid: bool) -> CachedValidationResult {
734 CachedValidationResult::new(focus, shape, valid, vec![], Duration::from_secs(60))
735 }
736
737 fn key(focus: &str, shape: &str) -> ValidationCacheKey {
738 ValidationCacheKey::new(focus, shape, 0)
739 }
740
741 #[test]
744 fn test_invalidate_node_removes_single_entry() {
745 let cache = ValidationCache::new(100, Duration::from_secs(60));
746 cache.put(
747 key("http://ex/Alice", "http://ex/S"),
748 entry("http://ex/Alice", "http://ex/S", true),
749 );
750 assert_eq!(cache.raw_size(), 1);
751
752 let removed = cache.invalidate_node("http://ex/Alice");
753 assert_eq!(removed, 1);
754 assert_eq!(cache.raw_size(), 0);
755 }
756
757 #[test]
758 fn test_invalidate_node_removes_multiple_shapes() {
759 let cache = ValidationCache::new(100, Duration::from_secs(60));
760 cache.put(
761 key("http://ex/Alice", "http://ex/S1"),
762 entry("http://ex/Alice", "http://ex/S1", true),
763 );
764 cache.put(
765 key("http://ex/Alice", "http://ex/S2"),
766 entry("http://ex/Alice", "http://ex/S2", false),
767 );
768 cache.put(
769 key("http://ex/Bob", "http://ex/S1"),
770 entry("http://ex/Bob", "http://ex/S1", true),
771 );
772
773 let removed = cache.invalidate_node("http://ex/Alice");
774 assert_eq!(removed, 2, "both Alice entries should be removed");
775 assert_eq!(cache.raw_size(), 1, "Bob entry should remain");
776 }
777
778 #[test]
779 fn test_invalidate_node_nonexistent_is_zero() {
780 let cache = ValidationCache::new(100, Duration::from_secs(60));
781 let removed = cache.invalidate_node("http://ex/NoSuchNode");
782 assert_eq!(removed, 0);
783 }
784
785 #[test]
788 fn test_invalidate_triple_removes_dependent_entries() {
789 let cache = ValidationCache::new(100, Duration::from_secs(60));
790 let triple_key = "http://ex/Alice/name/Bob";
791
792 let mut e = entry("http://ex/Alice", "http://ex/S", true);
793 e.add_accessed_triple(triple_key);
794
795 cache.put(key("http://ex/Alice", "http://ex/S"), e);
796 assert_eq!(cache.raw_size(), 1);
797
798 let removed = cache.invalidate_triple(triple_key);
799 assert_eq!(removed, 1);
800 assert_eq!(cache.raw_size(), 0);
801 }
802
803 #[test]
804 fn test_invalidate_triple_non_dependent_entry_stays() {
805 let cache = ValidationCache::new(100, Duration::from_secs(60));
806 cache.put(
808 key("http://ex/Bob", "http://ex/S"),
809 entry("http://ex/Bob", "http://ex/S", true),
810 );
811
812 let removed = cache.invalidate_triple("some:triple:key");
813 assert_eq!(removed, 0);
814 assert_eq!(cache.raw_size(), 1);
815 }
816
817 #[test]
820 fn test_evict_stale_removes_expired_entries() {
821 let cache = ValidationCache::new(100, Duration::from_secs(60));
822
823 let stale = CachedValidationResult::new(
825 "http://ex/Alice",
826 "http://ex/S",
827 true,
828 vec![],
829 Duration::ZERO, );
831 cache.put(key("http://ex/Alice", "http://ex/S"), stale);
832
833 cache.put(
835 key("http://ex/Bob", "http://ex/S"),
836 entry("http://ex/Bob", "http://ex/S", true),
837 );
838
839 let evicted = cache.evict_stale();
840 assert_eq!(evicted, 1, "one stale entry should be evicted");
841 }
842
843 #[test]
846 fn test_size_excludes_stale_entries() {
847 let cache = ValidationCache::new(100, Duration::from_secs(60));
848
849 let stale = CachedValidationResult::new(
850 "http://ex/Alice",
851 "http://ex/S",
852 true,
853 vec![],
854 Duration::ZERO,
855 );
856 cache.put(key("http://ex/Alice", "http://ex/S"), stale);
857 cache.put(
858 key("http://ex/Bob", "http://ex/S"),
859 entry("http://ex/Bob", "http://ex/S", true),
860 );
861
862 assert_eq!(cache.raw_size(), 2);
864 assert!(cache.size() <= 2);
866 }
867
868 #[test]
871 fn test_entry_is_stale_with_zero_ttl() {
872 let stale = CachedValidationResult::new(
873 "http://ex/Alice",
874 "http://ex/S",
875 true,
876 vec![],
877 Duration::ZERO,
878 );
879 assert!(stale.is_stale());
880 }
881
882 #[test]
883 fn test_entry_not_stale_with_large_ttl() {
884 let fresh = entry("http://ex/Alice", "http://ex/S", true);
885 assert!(!fresh.is_stale());
886 }
887
888 #[test]
889 fn test_remaining_ttl_is_zero_for_stale_entry() {
890 let stale = CachedValidationResult::new(
891 "http://ex/Alice",
892 "http://ex/S",
893 true,
894 vec![],
895 Duration::ZERO,
896 );
897 assert_eq!(stale.remaining_ttl(), Duration::ZERO);
898 }
899
900 #[test]
901 fn test_accessed_triples_recorded() {
902 let mut e = entry("http://ex/Alice", "http://ex/S", true);
903 e.add_accessed_triple("triple:a");
904 e.add_accessed_triple("triple:b");
905 assert_eq!(e.accessed_triples.len(), 2);
906 }
907
908 #[test]
911 fn test_cache_key_equality_same_inputs() {
912 let k1 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S", 0u64);
913 let k2 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S", 0u64);
914 assert_eq!(k1, k2);
915 }
916
917 #[test]
918 fn test_cache_key_inequality_different_shape() {
919 let k1 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S1", 0u64);
920 let k2 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S2", 0u64);
921 assert_ne!(k1, k2);
922 }
923
924 #[test]
925 fn test_cache_key_inequality_different_node() {
926 let k1 = ValidationCacheKey::new("http://ex/Alice", "http://ex/S", 0u64);
927 let k2 = ValidationCacheKey::new("http://ex/Bob", "http://ex/S", 0u64);
928 assert_ne!(k1, k2);
929 }
930
931 #[test]
934 fn test_stats_initial_zero() {
935 let cache = ValidationCache::new(100, Duration::from_secs(60));
936 let stats = cache.stats();
937 assert_eq!(stats.hit_count, 0);
938 assert_eq!(stats.miss_count, 0);
939 assert_eq!(stats.entries, 0);
940 }
941
942 #[test]
943 fn test_stats_put_count_increments() {
944 let cache = ValidationCache::new(100, Duration::from_secs(60));
945 cache.put(
946 key("http://ex/A", "http://ex/S"),
947 entry("http://ex/A", "http://ex/S", true),
948 );
949 cache.put(
950 key("http://ex/B", "http://ex/S"),
951 entry("http://ex/B", "http://ex/S", true),
952 );
953 let stats = cache.stats();
954 assert_eq!(stats.entries, 2);
955 }
956
957 #[test]
958 fn test_stats_miss_increments_on_absent_key() {
959 let cache = ValidationCache::new(100, Duration::from_secs(60));
960 let _ = cache.get(&key("http://ex/NoNode", "http://ex/S"));
961 let stats = cache.stats();
962 assert_eq!(stats.miss_count, 1);
963 }
964
965 #[test]
968 fn test_default_ttl_matches_constructor() {
969 let ttl = Duration::from_secs(120);
970 let cache = ValidationCache::new(50, ttl);
971 assert_eq!(cache.default_ttl(), ttl);
972 }
973
974 #[test]
977 fn test_concurrent_invalidation_safety() {
978 let cache = std::sync::Arc::new(ValidationCache::new(1000, Duration::from_secs(60)));
979
980 for i in 0..50 {
982 cache.put(
983 key(&format!("http://ex/Node{i}"), "http://ex/S"),
984 entry(&format!("http://ex/Node{i}"), "http://ex/S", true),
985 );
986 }
987
988 let cache2 = std::sync::Arc::clone(&cache);
989 let handle = thread::spawn(move || {
990 for i in 0..50 {
991 cache2.invalidate_node(&format!("http://ex/Node{i}"));
992 }
993 });
994
995 for i in 0..50 {
997 let _ = cache.get(&key(&format!("http://ex/Node{i}"), "http://ex/S"));
998 }
999
1000 handle.join().expect("thread should not panic");
1001 }
1002
1003 #[test]
1006 fn test_hit_rate_zero_when_no_accesses() {
1007 let cache = ValidationCache::new(100, Duration::from_secs(60));
1008 assert_eq!(cache.hit_rate(), 0.0);
1009 }
1010
1011 #[test]
1012 fn test_hit_rate_one_when_all_hits() {
1013 let cache = ValidationCache::new(100, Duration::from_secs(60));
1014 let k = key("http://ex/Alice", "http://ex/S");
1015 cache.put(k.clone(), entry("http://ex/Alice", "http://ex/S", true));
1016 let _ = cache.get(&k);
1017 let _ = cache.get(&k);
1018 assert!((cache.hit_rate() - 1.0).abs() < 1e-9);
1020 }
1021}