A write owns the record at its key when the written type declares the record's schema id, or reads an older undeclared id of its own schema name whole (ruling own-entry-reads-whole); every other registered type's record is refused, in every cache, with or without a backing store (cache-refuses-retype). Removing a record and adding one of another type in a single batch is staged as the held record's eviction then the add, so validators, Claim, the store judges and the change journal see what they see staged apart (one-batch-retype-is-eviction-then-add); Watch<old> gets Removed and Watch<new> Added. A one-batch retype across home stores is refused by the batch-spans rule and must be staged apart. A variant rebase changes type exactly where a plain write may (variant-rebase-follows-plain-write). Store kinds outside the library get one protected per-key judge (store-kind-per-key-judge), exercised by tests/GameCult.Caching.OutsideStoreTests. Spec variants:cut_spec:cut-write-ownership-cs.r1; reports h1-h5; Soul s5 clears the merge (verdict variants:verdict:cut-write-ownership-cs.s5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
4.7 KiB
C#
97 lines
4.7 KiB
C#
#nullable enable
|
|
using System;
|
|
using GameCult.Caching.MessagePack;
|
|
using MessagePack;
|
|
using NUnit.Framework;
|
|
|
|
namespace GameCult.Caching.Tests
|
|
{
|
|
// The one per-key predicate of write ownership (variants:ruling:write-ownership-by-written-type, own-entry-reads-whole), over
|
|
// catalogs the production types publish. Each refusal is paired with the accepted case that differs in only the one thing the
|
|
// rule looks at. Where the predicate is wired in is WriteOwnershipTests.
|
|
public class WrittenOwnsTests
|
|
{
|
|
// One schema name at three versions, nothing declared: N2 reads N1's records whole (it adds a slot) and Wide's only by
|
|
// dropping a slot; Renamed has N2's exact shape under another schema name.
|
|
[CultDocument("tests.wo_n", "tests.wo_n.v1")]
|
|
[MessagePackObject(AllowPrivate = true)]
|
|
internal sealed class WoN1
|
|
{
|
|
[Key(0)] [CultName] public string Name = string.Empty;
|
|
[Key(1)] public int Value;
|
|
}
|
|
|
|
[CultDocument("tests.wo_n", "tests.wo_n.v2")]
|
|
[MessagePackObject(AllowPrivate = true)]
|
|
internal sealed class WoN2
|
|
{
|
|
[Key(0)] [CultName] public string Name = string.Empty;
|
|
[Key(1)] public int Value;
|
|
[Key(2)] public string Colour = string.Empty;
|
|
}
|
|
|
|
[CultDocument("tests.wo_n", "tests.wo_n.v3")]
|
|
[MessagePackObject(AllowPrivate = true)]
|
|
internal sealed class WoNWide
|
|
{
|
|
[Key(0)] [CultName] public string Name = string.Empty;
|
|
[Key(1)] public int Value;
|
|
[Key(2)] public string Colour = string.Empty;
|
|
[Key(3)] public string Extra = string.Empty;
|
|
}
|
|
|
|
[CultDocument("tests.wo_nx", "tests.wo_nx.v1")]
|
|
[MessagePackObject(AllowPrivate = true)]
|
|
internal sealed class WoNRenamed
|
|
{
|
|
[Key(0)] [CultName] public string Name = string.Empty;
|
|
[Key(1)] public int Value;
|
|
[Key(2)] public string Colour = string.Empty;
|
|
}
|
|
|
|
private static CultSchemaCatalogEntry EntryOf<T>() where T : class =>
|
|
CultDocumentRegistry.ForTypes(new[] { typeof(T) }).GetRequired<T>().ToCatalogEntry();
|
|
|
|
private static CultSchemaCatalogEntry Listing(CultSchemaCatalogEntry entry, params string[] ids) =>
|
|
new()
|
|
{
|
|
SchemaId = entry.SchemaId,
|
|
ContentHash = entry.ContentHash,
|
|
SchemaName = entry.SchemaName,
|
|
SchemaVersion = entry.SchemaVersion,
|
|
CanonicalSchemaJson = entry.CanonicalSchemaJson,
|
|
Members = entry.Members,
|
|
CompatibleSchemaIds = ids,
|
|
};
|
|
|
|
[Test]
|
|
public void WrittenOwnsDeclaredIdsAreTheEntrysOwnOrAnyItListsNotOnlyTheFirst()
|
|
{
|
|
var a = EntryOf<WriteOwnershipTests.WoA>();
|
|
var b = EntryOf<WriteOwnershipTests.WoB>();
|
|
var catalog = new[] { a };
|
|
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(a, a.SchemaId, catalog), Is.True, "its own id");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(b, a.SchemaId, catalog), Is.False, "same shape, another schema: not its record");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(Listing(b, b.SchemaId, a.SchemaId), a.SchemaId, catalog), Is.True, "listed last");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(Listing(b, a.SchemaId, b.SchemaId), a.SchemaId, catalog), Is.True, "listed first");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(Listing(b, b.SchemaId, "tests.wo.other"), a.SchemaId, catalog), Is.False, "lists another id");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(a, a.SchemaId, Array.Empty<CultSchemaCatalogEntry>()), Is.True, "declared needs no catalog");
|
|
}
|
|
|
|
[Test]
|
|
public void WrittenOwnsAnOlderIdOfItsOwnNameOnlyWhenItReadsTheRecordWhole()
|
|
{
|
|
var n1 = EntryOf<WoN1>();
|
|
var n2 = EntryOf<WoN2>();
|
|
var wide = EntryOf<WoNWide>();
|
|
var renamed = EntryOf<WoNRenamed>();
|
|
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(n2, n1.SchemaId, new[] { n1 }), Is.True, "same name, reads whole");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(renamed, n1.SchemaId, new[] { n1 }), Is.False, "the same shape under another name");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(n2, wide.SchemaId, new[] { wide }), Is.False, "same name, drops a persisted slot");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(n2, "tests.wo.unpublished", new[] { n1 }), Is.False, "an id no catalog entry publishes");
|
|
Assert.That(CultDocumentRegistry.WrittenOwns(n2, n1.SchemaId, new[] { n1, wide }), Is.True, "another entry in the catalog changes nothing");
|
|
}
|
|
}
|
|
}
|