From da951adce04e9cbf538907c712a7e0ddba10b5bf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 27 Oct 2026 03:46:27 +0100 Subject: [PATCH] Tests: fixtures that were read-only because their type read the record whole now lose a slot; restamp tests fixed The ForeignRecordTests read-only fixtures (another runtime's deck, the moving deck) had the same shape as the deck, which the type now reads whole and owns; each gets a slot the deck lacks, so it is read only by dropping it. RestampOnWriteTests and the successful-churn gate test are corrected from the first run. Not run at this commit. Co-Authored-By: Claude Sonnet 5.5 --- .../CachelessSingleFileTests.cs | 8 +++-- .../ForeignRecordTests.cs | 10 +++--- .../RestampOnWriteTests.cs | 33 ++++++++++++++----- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs b/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs index ce45bdd5..a501be67 100644 --- a/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs +++ b/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs @@ -1446,21 +1446,24 @@ namespace GameCult.Caching.Tests [Test] public void OneStoreCallTakesAtMostThreeGatesUnderSuccessfulAttachChurnAndThreeIsReached() { + var rounds = 0; var registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) }); new SingleFileMessagePackBackingStore(_path).WriteDocument(new CultRecordKey("a"), new IdDeck { Name = "a" }, registry: registry); var mostTakes = 0; var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(60); for (var round = 0; mostTakes < 3 && DateTime.UtcNow < deadline; round++) { + rounds++; var store = new RoleStore(_path); store.ReleaseAttach.Set(); var cache = new CultCache(registry); using var barrier = new Barrier(2); var takes = 0; - var spin = (round % 16) * 40; + var spin = (round % 128) * 20; var caller = new Thread(() => { barrier.SignalAndWait(); + Thread.SpinWait(spin); CacheBackingStore.GateTakes = 0; store.ReadStoreSnapshot(); takes = CacheBackingStore.GateTakes; @@ -1468,7 +1471,6 @@ namespace GameCult.Caching.Tests var attacher = new Thread(() => { barrier.SignalAndWait(); - Thread.SpinWait(spin); cache.AddBackingStore(store); }); caller.Start(); @@ -1480,7 +1482,7 @@ namespace GameCult.Caching.Tests mostTakes = Math.Max(mostTakes, takes); } - Assert.That(mostTakes, Is.EqualTo(3), "a call that raced a successful attach took the third gate"); + Assert.That(mostTakes, Is.EqualTo(3), $"a call that raced a successful attach took the third gate ({rounds} rounds)"); } // Under attaches that fail, Cache read under the store's gate is always null (an attach publishes and retracts it inside one diff --git a/tests/GameCult.Caching.Tests/ForeignRecordTests.cs b/tests/GameCult.Caching.Tests/ForeignRecordTests.cs index 4c30b9e0..ab4cb046 100644 --- a/tests/GameCult.Caching.Tests/ForeignRecordTests.cs +++ b/tests/GameCult.Caching.Tests/ForeignRecordTests.cs @@ -27,8 +27,9 @@ namespace GameCult.Caching.Tests private static readonly string WidgetId = Full.GetRequired(Widget).SchemaId; // Another writer's deck (an older or newer build, another runtime) under an id the deck does not declare. It declares the - // deck's own id compatible, so it may rewrite the deck's records; the deck still reads them through that listing, read-only. - private static readonly Type MovingDeck = Emit("ForeignSuiteMovingDeck", "tests.foreign_deck", "tests.foreign_deck.v3", new[] { new Field("Name", typeof(string), 0, IsName: true) }, new[] { DeckOnly.GetRequired(Deck).SchemaId }); + // deck's own id compatible, so it may rewrite the deck's records; the deck still reads them through that listing, but only by + // dropping the Note slot it lacks, so it does not own them and holds them read-only. + private static readonly Type MovingDeck = Emit("ForeignSuiteMovingDeck", "tests.foreign_deck", "tests.foreign_deck.v3", new[] { new Field("Name", typeof(string), 0, IsName: true), new Field("Note", typeof(string), 1) }, new[] { DeckOnly.GetRequired(Deck).SchemaId }); private static readonly CultDocumentRegistry Moving = CultDocumentRegistry.ForTypes(new[] { MovingDeck }); private static readonly string MovingId = Moving.GetRequired(MovingDeck).SchemaId; @@ -587,8 +588,8 @@ namespace GameCult.Caching.Tests private const string OtherRuntimeId = "other.runtime.deck"; - // A deck another runtime wrote under its own id for the deck's schema. This build reads it through the catalog's schema name, - // but does not own the id. + // A deck another runtime wrote under its own id for the deck's schema, with a slot this build lacks. This build reads it + // through the catalog's schema name, but only by dropping that slot, so it does not own the id. private string OtherRuntimeStore(string name, bool withVariant = false) { var path = PathOf(name); @@ -604,6 +605,7 @@ namespace GameCult.Caching.Tests entry.SchemaId = OtherRuntimeId; entry.ContentHash = OtherRuntimeId; entry.CompatibleSchemaIds = new[] { OtherRuntimeId }; + entry.Members = entry.Members.Append(new CultSchemaMemberCatalogEntry { Slot = 99, MemberName = "Dropped", TypeName = "System.String" }).ToArray(); foreach (var record in snapshot.Records) record.SchemaId = OtherRuntimeId; // An older entry that also lists the id, first in the catalog: the entry that owns the id is the one that publishes it. diff --git a/tests/GameCult.Caching.Tests/RestampOnWriteTests.cs b/tests/GameCult.Caching.Tests/RestampOnWriteTests.cs index 05fc89bb..83d808ca 100644 --- a/tests/GameCult.Caching.Tests/RestampOnWriteTests.cs +++ b/tests/GameCult.Caching.Tests/RestampOnWriteTests.cs @@ -184,7 +184,8 @@ namespace GameCult.Caching.Tests public void ADeclaredLossyDriftRecordIsWritable(bool directory) { var path = SeedWide(directory); - Assert.That(WideRegistry.GetRequired().SchemaId, Is.EqualTo(WideId), "the declaration names the lossy record's id"); + var wideId = WideRegistry.GetRequired().SchemaId; + Assert.That(wideId, Is.EqualTo(WideId), "the declaration names the lossy record's id: " + wideId); using (var cache = Open(path, directory, DeclaringRegistry)) Assert.That(cache.Commit(batch => batch.Upsert(new NoteGrownDeclaring { Name = "note", Value = 2 }, new CultRecordHandle(Note))), Is.True); @@ -236,18 +237,34 @@ namespace GameCult.Caching.Tests public void ExpectAgreesWithRestamp(bool directory) { var path = SeedNarrow(directory); + var third = new CultRecordKey("third"); using var cache = Open(path, directory, GrownRegistry); - using var second = Open(path, directory, GrownRegistry); - var observed = cache.Get(Note)!; - Assert.That(cache.Commit(batch => batch.ExpectUnchanged()), Is.True, "an unchanged drift record satisfies ExpectUnchanged"); Assert.That(cache.Commit(batch => { - batch.Expect(Note, observed); + batch.Expect(Note, cache.Get(Note)); + batch.ExpectUnchanged(); + batch.Upsert(new NoteGrown { Name = "third" }, new CultRecordHandle(third)); + }), Is.True, "an unchanged drift record satisfies Expect and ExpectUnchanged"); + + using var second = Open(path, directory, GrownRegistry); + Assert.That(cache.Commit(batch => + { + batch.Expect(Note, cache.Get(Note)); batch.Upsert(new NoteGrown { Name = "note", Value = 2 }, new CultRecordHandle(Note)); - }), Is.True, "a batch that Expects the drift record and upserts it commits and restamps"); - Assert.That(cache.Commit(batch => batch.ExpectUnchanged()), Is.True, "the writer's later ExpectUnchanged holds"); - Assert.That(second.Commit(batch => batch.ExpectUnchanged()), Is.False, "a cache loaded before the restamp sees the bytes change"); + }), Is.True, "a batch that Expects the drift record and upserts it commits"); + Assert.That(StoredIdOf(path, directory, GrownRegistry, Note), Is.EqualTo(GrownRegistry.GetRequired().SchemaId), "and restamps it"); + + Assert.That(cache.Commit(batch => + { + batch.ExpectUnchanged(); + batch.Upsert(new NoteGrown { Name = "fourth" }, new CultRecordHandle(new CultRecordKey("fourth"))); + }), Is.True, "the writer's later ExpectUnchanged holds"); + Assert.That(second.Commit(batch => + { + batch.Expect(Note, second.Get(Note)); + batch.Upsert(new NoteGrown { Name = "note", Value = 3 }, new CultRecordHandle(Note)); + }), Is.False, "a cache loaded before the restamp fails its Expect: the bytes changed"); } private static string VectorPath(string name)