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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-27 03:46:27 +01:00
commit da951adce0
3 changed files with 36 additions and 15 deletions

View file

@ -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

View file

@ -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.

View file

@ -184,7 +184,8 @@ namespace GameCult.Caching.Tests
public void ADeclaredLossyDriftRecordIsWritable(bool directory)
{
var path = SeedWide(directory);
Assert.That(WideRegistry.GetRequired<NoteWide>().SchemaId, Is.EqualTo(WideId), "the declaration names the lossy record's id");
var wideId = WideRegistry.GetRequired<NoteWide>().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<NoteGrownDeclaring>(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<NoteGrown>(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<NoteGrown>(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<NoteGrown>().SchemaId), "and restamps it");
Assert.That(cache.Commit(batch =>
{
batch.ExpectUnchanged();
batch.Upsert(new NoteGrown { Name = "fourth" }, new CultRecordHandle<NoteGrown>(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<NoteGrown>(Note));
}), Is.False, "a cache loaded before the restamp fails its Expect: the bytes changed");
}
private static string VectorPath(string name)