// Go reference: server/client.go routeCache, maxResultCacheSize=8192. using NATS.Server.Subscriptions; using Shouldly; namespace NATS.Server.Core.Tests.Subscriptions; /// /// Unit tests for RouteResultCache — the per-account LRU cache that stores /// SubListResult objects keyed by subject string to avoid repeated SubList.Match() calls. /// Go reference: server/client.go routeCache field, maxResultCacheSize=8192. /// public class RouteResultCacheTests { // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private static SubListResult MakeResult(string subject) { var sub = new Subscription { Subject = subject, Sid = "1" }; return new SubListResult([sub], []); } // ========================================================================= // Basic lookup behaviour // ========================================================================= /// /// An empty cache must return false for any subject. /// [Fact] public void TryGet_returns_false_on_empty_cache() { var cache = new RouteResultCache(capacity: 16); var found = cache.TryGet("foo.bar", out var result); found.ShouldBeFalse(); result.ShouldBeNull(); } /// /// A result placed with Set must be retrievable via TryGet. /// [Fact] public void Set_and_TryGet_round_trips_result() { var cache = new RouteResultCache(capacity: 16); var expected = MakeResult("foo"); cache.Set("foo", expected); var found = cache.TryGet("foo", out var actual); found.ShouldBeTrue(); actual.ShouldBeSameAs(expected); } // ========================================================================= // LRU eviction // ========================================================================= /// /// When a capacity+1 entry is added, the oldest (LRU) entry must be evicted. /// Go reference: maxResultCacheSize=8192 with LRU eviction. /// [Fact] public void LRU_eviction_at_capacity() { const int cap = 4; var cache = new RouteResultCache(capacity: cap); // Fill to capacity — "first" is the oldest (LRU) cache.Set("first", MakeResult("first")); cache.Set("second", MakeResult("second")); cache.Set("third", MakeResult("third")); cache.Set("fourth", MakeResult("fourth")); // Adding a 5th entry must evict "first" cache.Set("fifth", MakeResult("fifth")); cache.Count.ShouldBe(cap); cache.TryGet("first", out _).ShouldBeFalse(); cache.TryGet("fifth", out _).ShouldBeTrue(); } /// /// After accessing A, B, C (capacity=3), accessing A promotes it to MRU. /// The next Set evicts B (now the true LRU) rather than A. /// [Fact] public void LRU_order_updated_on_access() { var cache = new RouteResultCache(capacity: 3); cache.Set("A", MakeResult("A")); cache.Set("B", MakeResult("B")); cache.Set("C", MakeResult("C")); // Access A so it becomes MRU; now LRU order is: B, A, C (front=C, back=B) cache.TryGet("A", out _); // D evicts the LRU entry — should be B, not A cache.Set("D", MakeResult("D")); cache.TryGet("A", out _).ShouldBeTrue(); cache.TryGet("B", out _).ShouldBeFalse(); cache.TryGet("C", out _).ShouldBeTrue(); cache.TryGet("D", out _).ShouldBeTrue(); } // ========================================================================= // Generation / invalidation // ========================================================================= /// /// After Invalidate(), the next TryGet must detect the generation mismatch, /// clear all entries, and return false. /// [Fact] public void Invalidate_clears_on_next_access() { var cache = new RouteResultCache(capacity: 16); cache.Set("a", MakeResult("a")); cache.Set("b", MakeResult("b")); cache.Invalidate(); cache.TryGet("a", out var result).ShouldBeFalse(); result.ShouldBeNull(); cache.Count.ShouldBe(0); } /// /// Each call to Invalidate() must increment the generation counter by 1. /// [Fact] public void Generation_increments_on_invalidate() { var cache = new RouteResultCache(); var gen0 = cache.Generation; cache.Invalidate(); cache.Generation.ShouldBe(gen0 + 1); cache.Invalidate(); cache.Generation.ShouldBe(gen0 + 2); } // ========================================================================= // Statistics // ========================================================================= /// /// Hits counter increments on each successful TryGet. /// [Fact] public void Hits_incremented_on_cache_hit() { var cache = new RouteResultCache(capacity: 16); cache.Set("subject", MakeResult("subject")); cache.TryGet("subject", out _); cache.TryGet("subject", out _); cache.Hits.ShouldBe(2); } /// /// Misses counter increments on each failed TryGet. /// [Fact] public void Misses_incremented_on_cache_miss() { var cache = new RouteResultCache(capacity: 16); cache.TryGet("nope", out _); cache.TryGet("also.nope", out _); cache.Misses.ShouldBe(2); } // ========================================================================= // Clear // ========================================================================= /// /// Clear() must remove all entries immediately. /// [Fact] public void Clear_removes_all_entries() { var cache = new RouteResultCache(capacity: 16); cache.Set("x", MakeResult("x")); cache.Set("y", MakeResult("y")); cache.Clear(); cache.Count.ShouldBe(0); cache.TryGet("x", out _).ShouldBeFalse(); } // ========================================================================= // Update existing entry // ========================================================================= /// /// Setting the same key twice must replace the value; TryGet returns the latest result. /// [Fact] public void Set_updates_existing_entry() { var cache = new RouteResultCache(capacity: 16); var first = MakeResult("v1"); var second = MakeResult("v2"); cache.Set("key", first); cache.Set("key", second); cache.Count.ShouldBe(1); cache.TryGet("key", out var returned).ShouldBeTrue(); returned.ShouldBeSameAs(second); } }