diff --git a/tests/GameCult.Caching.Tests/CultCacheJournalTests.cs b/tests/GameCult.Caching.Tests/CultCacheJournalTests.cs index 563669e0..287af93b 100644 --- a/tests/GameCult.Caching.Tests/CultCacheJournalTests.cs +++ b/tests/GameCult.Caching.Tests/CultCacheJournalTests.cs @@ -124,12 +124,10 @@ namespace GameCult.Caching.Tests { using var cache = Memory(); var calls = 0; - using var journal = cache.AddJournal(changes => - { - calls++; - Assert.That(changes, Is.Not.Empty); - }); + using var journal = cache.AddJournal(_ => calls++); + // Attaching a store with nothing in it is an admission with no change. + cache.AddBackingStore(new SingleFileMessagePackBackingStore(Path.Combine(_directory, "empty.cc"))); Assert.That(cache.Remove(KeyOf("absent")), Is.False); Assert.That(calls, Is.Zero); diff --git a/tests/GameCult.Caching.Tests/CultCacheObserverTests.cs b/tests/GameCult.Caching.Tests/CultCacheObserverTests.cs index fa87ea4f..b26d4ca4 100644 --- a/tests/GameCult.Caching.Tests/CultCacheObserverTests.cs +++ b/tests/GameCult.Caching.Tests/CultCacheObserverTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using GameCult.Caching; using GameCult.Caching.MessagePack; using MessagePack; @@ -123,17 +124,36 @@ namespace GameCult.Caching.Tests } [Test] - public void AnUnsubscribedObserverStopsReceiving() + public void AnUnsubscribedObserverIsReleasedByTheCache() { using var cache = new CultCache(Registry); - var seen = new List(); - var subscription = cache.Watch().Subscribe(change => seen.Add(change.Document!.Text)); + var target = Subscribe(cache, out var subscription); Send(cache, "one"); + CollectGarbage(); + Assert.That(target.IsAlive, Is.True, "a live subscription is held by the cache"); subscription.Dispose(); - Send(cache, "two"); + CollectGarbage(); - Assert.That(seen, Is.EqualTo(new[] { "one" })); + Assert.That(target.IsAlive, Is.False, "an unsubscribed observer is no longer held"); + GC.KeepAlive(cache); + } + + // A disposed R3 subscription ignores OnNext, so a stale entry in the cache is invisible to a receiver; it shows as + // a leak: the cache keeps the subscriber alive. + [MethodImpl(MethodImplOptions.NoInlining)] + private static WeakReference Subscribe(CultCache cache, out IDisposable subscription) + { + var target = new object(); + subscription = cache.Watch().Subscribe(_ => GC.KeepAlive(target)); + return new WeakReference(target); + } + + private static void CollectGarbage() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); } // A store that admits a load and then fails: the hold's body throws after the changes were minted.