From d3edf80ed385a09e22c1477ddc0f7ee2d9d25a86 Mon Sep 17 00:00:00 2001 From: Libby Date: Fri, 13 Nov 2026 16:06:57 +0000 Subject: [PATCH] Caching tests: pin the variant refusal message and ReadDurable leaving the store view alone; start TwoWriters rounds together The directory race test no longer collided once each attempt reads fresh, so each round now starts both writers at a barrier. Co-Authored-By: Claude Sonnet 5.5 --- tests/GameCult.Caching.Tests/UpdateTests.cs | 27 ++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/GameCult.Caching.Tests/UpdateTests.cs b/tests/GameCult.Caching.Tests/UpdateTests.cs index 3957d54b..828f03bf 100644 --- a/tests/GameCult.Caching.Tests/UpdateTests.cs +++ b/tests/GameCult.Caching.Tests/UpdateTests.cs @@ -78,16 +78,22 @@ namespace GameCult.Caching.Tests using var a = Open(path, directory); using var b = Open(path, directory); var applied = 0; + // Each round starts both writers together: an attempt reads fresh from the store, so only overlapping read-commit windows lose. + var round = new Barrier(2); var racers = new[] { a, b }.Select(cache => Task.Run(() => { var results = new List(); for (var i = 0; i < n; i++) + { + round.SignalAndWait(); results.Add(cache.Update(Key, counter => { Interlocked.Increment(ref applied); return Plus(counter, 1); }, maxAttempts: 200).Outcome); + } + return results; })).ToArray(); Assert.That(Task.WaitAll(racers, TimeSpan.FromMinutes(2)), Is.True, "the racers did not finish"); @@ -252,7 +258,7 @@ namespace GameCult.Caching.Tests before = File.ReadAllBytes(path); var gearCalls = 0; using (var cache = Open(path, false)) - Assert.That(() => cache.Update(new CultRecordKey("gear-big"), gear => { gearCalls++; return gear; }), Throws.InvalidOperationException); + Assert.That(() => cache.Update(new CultRecordKey("gear-big"), gear => { gearCalls++; return gear; }), Throws.InvalidOperationException.With.Message.Contains("Update cannot copy it")); Assert.That(gearCalls, Is.EqualTo(0), "apply ran for a variant"); Assert.That(calls, Is.EqualTo(0), "apply ran for a refused call"); Assert.That(File.ReadAllBytes(path), Is.EqualTo(before)); @@ -509,6 +515,25 @@ namespace GameCult.Caching.Tests Assert.That(StoredValue(path, directory), Is.EqualTo(8)); } + // The durable read is not a load: it leaves the store's view of what the cache holds alone, so a later pull still delivers the rival's write. + [TestCase(false)] + [TestCase(true)] + public void ADeclinedUpdateDoesNotHideTheRivalsWriteFromALaterPull(bool directory) + { + var path = PathOf("hidden.cc"); + Seed(path, directory, 1); + using var a = Open(path, directory); + using var b = Open(path, directory); + Assert.That(b.Get(Key)!.Value, Is.EqualTo(1)); + Rival(a, 7); + + Assert.That(b.Update(Key, _ => null).Outcome, Is.EqualTo(CultUpdateOutcome.Declined)); + Assert.That(b.Get(Key)!.Value, Is.EqualTo(1), "a declined Update changed what the cache holds"); + b.PullAllBackingStoresAsync().GetAwaiter().GetResult(); + + Assert.That(b.Get(Key)!.Value, Is.EqualTo(7)); + } + [Test] public void CodecLessStoreBackedCacheUpdates() {