Slot tests, red: a resolver-formatted type and a [MessagePackObject] collection are not measured; flatten-chain vector row
ResolvedBag (an assembly CultCacheFormatterResolver writes [count, items...]) and TagList (List<string> with [MessagePackObject]) are falsely marked by the attribute-based guard (Soul s2, guard-asks-attribute-not-formatter). extra-slot-store.cc gains marked-variant, a variant of marked; slot-outcomes.msgpack rows gain a ninth column, the key whose refusal refuses a Flatten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bbfe335388
commit
0dfc437bfc
5 changed files with 64 additions and 10 deletions
|
|
@ -10,6 +10,7 @@ using MessagePack.Formatters;
|
|||
[assembly: CultCacheFormatterResolver(typeof(UnionArmGuardTests.OrderResolver))]
|
||||
[assembly: CultCacheFormatterResolver(typeof(UnionArmGuardTests.ListClaimResolver))]
|
||||
[assembly: CultCacheFormatterResolver(typeof(UnionArmGuardTests.FbLaterClaimResolver))]
|
||||
[assembly: CultCacheFormatterResolver(typeof(SlotReadOnlyTests.ResolvedBagResolver))]
|
||||
|
||||
namespace GameCult.Caching.Tests
|
||||
{
|
||||
|
|
|
|||
|
|
@ -160,10 +160,10 @@ namespace GameCult.Caching.Tests
|
|||
{
|
||||
var reader = new MessagePackReader(VectorFiles.Committed("union-parity", "slot-outcomes.msgpack"));
|
||||
var rows = reader.ReadArrayHeader();
|
||||
Assert.That(rows, Is.EqualTo(7));
|
||||
Assert.That(rows, Is.EqualTo(8));
|
||||
for (var i = 0; i < rows; i++)
|
||||
{
|
||||
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(8));
|
||||
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(9));
|
||||
var store = reader.ReadString()!;
|
||||
var key = new CultRecordKey(reader.ReadString()!);
|
||||
var writable = reader.ReadBoolean();
|
||||
|
|
@ -172,6 +172,7 @@ namespace GameCult.Caching.Tests
|
|||
var counts = new int?[3];
|
||||
for (var c = 0; c < 3; c++) counts[c] = reader.TryReadNil() ? null : reader.ReadInt32();
|
||||
var (slots, declared, occurrences) = (counts[0], counts[1], counts[2]);
|
||||
var flattenRefusedBy = reader.TryReadNil() ? null : reader.ReadString();
|
||||
var row = $"{store} {key.Value}";
|
||||
|
||||
var path = Path.Combine(_directory, $"{i}-{store}");
|
||||
|
|
@ -192,6 +193,13 @@ namespace GameCult.Caching.Tests
|
|||
|
||||
Assert.That(cache.IsWritable(key), Is.EqualTo(writable), row);
|
||||
Assert.That(cache.IsWritable(Unheld), Is.True, "a key no store holds is writable");
|
||||
if (flattenRefusedBy != null)
|
||||
{
|
||||
var refusal = Assert.Throws<CultSchemaConflictException>(() => cache.Commit(batch => batch.Flatten(key)), $"{row}: flatten")!;
|
||||
Assert.That(refusal.RecordKey, Is.EqualTo(flattenRefusedBy), $"{row}: the refusal is the base's own");
|
||||
Assert.That(File.ReadAllBytes(path), Is.EqualTo(before), $"{row}: a refused flatten leaves the store as it was");
|
||||
}
|
||||
|
||||
if (writable)
|
||||
{
|
||||
Assert.That(cache.Commit(batch => batch.Remove(key)), Is.True, row);
|
||||
|
|
@ -305,7 +313,6 @@ namespace GameCult.Caching.Tests
|
|||
reader.AddBackingStore(Store());
|
||||
var unread = Stored(reader, Marked).UnreadSlots;
|
||||
Assert.That(unread, Is.Not.Null, "the store kind decodes through the marking path");
|
||||
Assert.That(unread!.Explained, Is.True, "depth 0 and reported by the catalog comparison");
|
||||
Assert.That(unread.TypeName, Is.EqualTo(nameof(NarrowDoc)));
|
||||
Assert.That(unread.SlotCount, Is.EqualTo(3));
|
||||
Assert.That(unread.DeclaredCount, Is.EqualTo(2));
|
||||
|
|
@ -369,6 +376,37 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
}
|
||||
|
||||
// A [MessagePackObject] type an assembly resolver formats as [count, items...], with no [MessagePackFormatter] attribute: the
|
||||
// resolver owns its layout.
|
||||
[MessagePackObject]
|
||||
public sealed class ResolvedBag
|
||||
{
|
||||
[Key(0)] public List<string> Items = new();
|
||||
}
|
||||
|
||||
public sealed class ResolvedBagResolver : IFormatterResolver
|
||||
{
|
||||
public static readonly IFormatterResolver Instance = new ResolvedBagResolver();
|
||||
private static readonly IMessagePackFormatter<ResolvedBag?> Formatter = new ResolvedBagFormatter();
|
||||
|
||||
public IMessagePackFormatter<T>? GetFormatter<T>() => typeof(T) == typeof(ResolvedBag) ? (IMessagePackFormatter<T>)Formatter : null;
|
||||
|
||||
private sealed class ResolvedBagFormatter : IMessagePackFormatter<ResolvedBag?>
|
||||
{
|
||||
private static readonly BagFormatter Bag = new();
|
||||
|
||||
public void Serialize(ref MessagePackWriter writer, ResolvedBag? value, MessagePackSerializerOptions options) =>
|
||||
Bag.Serialize(ref writer, value == null ? null : new Bag { Items = value.Items }, options);
|
||||
|
||||
public ResolvedBag? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) =>
|
||||
Bag.Deserialize(ref reader, options) is { } bag ? new ResolvedBag { Items = bag.Items } : null;
|
||||
}
|
||||
}
|
||||
|
||||
// A [MessagePackObject] collection: MessagePack writes it as its items, not by [Key].
|
||||
[MessagePackObject]
|
||||
public class TagList : List<string> { }
|
||||
|
||||
[CultDocument("tests.slot_measured", "tests.slot_measured.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class MeasuredDoc
|
||||
|
|
@ -378,6 +416,8 @@ namespace GameCult.Caching.Tests
|
|||
[Key(2)] public Counted? Counted;
|
||||
[Key(3)] public Uncounted? Uncounted;
|
||||
[Key(4)] public Bag? Bag;
|
||||
[Key(5)] public ResolvedBag? Resolved;
|
||||
[Key(6)] public TagList? Tags;
|
||||
}
|
||||
|
||||
private static readonly CultDocumentRegistry MeasuredRegistry = CultDocumentRegistry.ForTypes(new[] { typeof(MeasuredDoc) });
|
||||
|
|
@ -388,6 +428,8 @@ namespace GameCult.Caching.Tests
|
|||
"constructed" => new MeasuredDoc { Name = member, Counted = new Counted { Count = 2 } },
|
||||
"unmeasurable" => new MeasuredDoc { Name = member, Uncounted = new Uncounted(2) },
|
||||
"own-formatter" => new MeasuredDoc { Name = member, Bag = new Bag { Items = { "x", "y", "z" } } },
|
||||
"resolver-formatted" => new MeasuredDoc { Name = member, Resolved = new ResolvedBag { Items = { "x", "y", "z" } } },
|
||||
"collection" => new MeasuredDoc { Name = member, Tags = new TagList { "a", "b", "c" } },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(member), member, null)
|
||||
};
|
||||
|
||||
|
|
@ -395,6 +437,8 @@ namespace GameCult.Caching.Tests
|
|||
[TestCase("constructed", TestName = "ATypeMeasuredThroughItsConstructorDoesNotMark")]
|
||||
[TestCase("unmeasurable", TestName = "AnUnmeasurableTypeIsNotGuarded")]
|
||||
[TestCase("own-formatter", TestName = "ATypeWithItsOwnFormatterIsNotMeasuredByKeys")]
|
||||
[TestCase("resolver-formatted", TestName = "ATypeAResolverFormatsIsNotMeasuredByKeys")]
|
||||
[TestCase("collection", TestName = "AMessagePackObjectCollectionIsNotMeasuredByKeys")]
|
||||
public void ARecordWrittenByItsOwnTypeDoesNotMark(string member)
|
||||
{
|
||||
var path = Path.Combine(_directory, "measured.cc");
|
||||
|
|
|
|||
|
|
@ -378,14 +378,16 @@ namespace GameCult.Caching.Tests
|
|||
_ => throw new ArgumentOutOfRangeException(nameof(store), store, null)
|
||||
};
|
||||
|
||||
// One row of slot-outcomes.msgpack: what the store's reader holds at the key. The mark fields are null for an unmarked record.
|
||||
private sealed record SlotOutcome(string Store, string Key, bool Writable, bool? Explained, string? Type, int? Slots, int? Declared, int? Occurrences);
|
||||
// One row of slot-outcomes.msgpack: what the store's reader holds at the key. The mark fields are null for an unmarked record;
|
||||
// FlattenRefusedBy is the key whose refusal refuses a Flatten of the record, null when the record is no variant or flattens.
|
||||
private sealed record SlotOutcome(string Store, string Key, bool Writable, bool? Explained, string? Type, int? Slots, int? Declared, int? Occurrences, string? FlattenRefusedBy = null);
|
||||
|
||||
private static readonly SlotOutcome[] SlotOutcomes =
|
||||
{
|
||||
new("extra-slot-store.cc", "marked", false, false, nameof(Composition), 3, 2, 1),
|
||||
new("extra-slot-store.cc", "clean", true, null, null, null, null, null),
|
||||
new("extra-slot-store.cc", "unit-arm", false, false, nameof(Denied2), 1, 0, 1),
|
||||
new("extra-slot-store.cc", "marked-variant", true, null, null, null, null, null, "marked"),
|
||||
new("extra-slot-consent.cc", "consented", true, true, nameof(SurfaceDeclaring), 8, 7, 1),
|
||||
new("extra-slot-consent.cc", "over-consent", false, false, nameof(SurfaceDeclaring), 9, 7, 1),
|
||||
new("extra-slot-consent.cc", "nested", false, false, nameof(SurfaceDeclaring), 8, 7, 2),
|
||||
|
|
@ -397,12 +399,13 @@ namespace GameCult.Caching.Tests
|
|||
w.WriteArrayHeader(SlotOutcomes.Length);
|
||||
foreach (var row in SlotOutcomes)
|
||||
{
|
||||
w.WriteArrayHeader(8);
|
||||
w.WriteArrayHeader(9);
|
||||
w.Write(row.Store); w.Write(row.Key); w.Write(row.Writable);
|
||||
if (row.Explained is { } explained) w.Write(explained); else w.WriteNil();
|
||||
if (row.Type is { } type) w.Write(type); else w.WriteNil();
|
||||
foreach (var count in new[] { row.Slots, row.Declared, row.Occurrences })
|
||||
if (count is { } value) w.Write(value); else w.WriteNil();
|
||||
if (row.FlattenRefusedBy is { } refusedBy) w.Write(refusedBy); else w.WriteNil();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -482,8 +485,10 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
// A single-file store of the records, each written as its type's value with the store's clock pinned as union-surface.cc's, then
|
||||
// its payload replaced by bytes no current writer produces; edit changes the snapshot further before it is serialized.
|
||||
private static byte[] SlotStore(CultDocumentRegistry registry, (string Key, object Value, byte[] Payload)[] records, Action<CultPersistedStoreSnapshot>? edit = null)
|
||||
// its payload replaced by bytes no current writer produces; each variant is written over its base (overriding the name to its
|
||||
// key) before the payloads are replaced; edit changes the snapshot further before it is serialized.
|
||||
private static byte[] SlotStore(CultDocumentRegistry registry, (string Key, object Value, byte[] Payload)[] records, Action<CultPersistedStoreSnapshot>? edit = null,
|
||||
params (string Key, string Base)[] variants)
|
||||
{
|
||||
var work = Path.Combine(Path.GetTempPath(), $"cultlib-unionparity-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(work);
|
||||
|
|
@ -495,6 +500,9 @@ namespace GameCult.Caching.Tests
|
|||
cache.Clock = () => StoredAt;
|
||||
foreach (var (key, value, _) in records)
|
||||
cache.UpsertAsync(value.GetType(), value, new CultRecordKey(key)).GetAwaiter().GetResult();
|
||||
foreach (var (key, baseKey) in variants)
|
||||
Assert.That(cache.Commit(batch => batch.UpsertVariant(new CultRecordKey(key), new CultRecordKey(baseKey),
|
||||
new[] { cache.Override(typeof(UnionSurface), nameof(UnionSurface.Id), key) })), Is.True);
|
||||
cache.FlushAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
|
@ -511,7 +519,8 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
// The store the slot-readonly tests open and the other runtimes read: "marked" holds the extra-slot-nested-record payload,
|
||||
// "clean" the ordinary value, and "unit-arm" a tests.slot_unit document ["unit-arm", [2, ["extra-slot"]]].
|
||||
// "clean" the ordinary value, "unit-arm" a tests.slot_unit document ["unit-arm", [2, ["extra-slot"]]], and "marked-variant" a
|
||||
// variant of "marked" overriding only its Id: writable, but a flatten of it is refused for its base.
|
||||
[Test]
|
||||
public void ExtraSlotStoreVectorHoldsAMarkedACleanAndAUnitArmRecord()
|
||||
{
|
||||
|
|
@ -520,7 +529,7 @@ namespace GameCult.Caching.Tests
|
|||
("marked", Constructed(), Committed("extra-slot-nested-record.msgpack")),
|
||||
("clean", Constructed(), Surface()),
|
||||
("unit-arm", new UnitDoc { Name = "unit-arm", Outcome = new Denied2() }, Raw(Slots(Str("unit-arm"), ArmOf(2, ExtraSlot)))),
|
||||
}));
|
||||
}, null, ("marked-variant", "marked")));
|
||||
}
|
||||
|
||||
// The consent store: every record under the wide id, whose catalog entry is UnionSurface's 7 members plus slot 7. Its reader
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue