GameCult.Thing.Contract: the name set (tests/thing-contract-parity) and NameSetTests
catalog-members.tsv (27 rows) and type-names.txt (168 types) are generated from the C# types on Yggdrasil at CultLib b3bd8dd5 and read by NameSetTests; the package owns the names, every other runtime is held to them. NameSetTests also patches System.Int64 to System.Int32 in a store C# wrote and requires reopen to refuse. Presence and list tests no longer count ThingDomainException as a door. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fe376ce9bd
commit
9586392d14
7 changed files with 286 additions and 2 deletions
|
|
@ -35,7 +35,7 @@ internal static class Doors
|
|||
public static IReadOnlyList<Door> All()
|
||||
{
|
||||
var doors = new List<Door>();
|
||||
foreach (var type in Package.GetExportedTypes().Where(t => !t.IsAbstract || t.IsSealed))
|
||||
foreach (var type in Package.GetExportedTypes().Where(t => (!t.IsAbstract || t.IsSealed) && t != typeof(ThingDomainException)))
|
||||
{
|
||||
foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance))
|
||||
doors.Add(new Door($"new {type.Name}", ctor));
|
||||
|
|
|
|||
77
tests/GameCult.Thing.Contract.Tests/NameSetTests.cs
Normal file
77
tests/GameCult.Thing.Contract.Tests/NameSetTests.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
namespace GameCult.Thing.Contract.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The names a CultCache store records for the package's documents. CultLib's C# reference compares catalog type names
|
||||
/// when a store reopens, so these names are the C# package's to own: tests/thing-contract-parity holds them for every
|
||||
/// runtime package, and they change only in the commit that changes the C# types.
|
||||
/// </summary>
|
||||
public sealed class NameSetTests
|
||||
{
|
||||
private static string ParityPath(string file) => Path.Combine(AppContext.BaseDirectory, "parity", file);
|
||||
|
||||
private static string[] Lines(string file) => File.ReadAllLines(ParityPath(file)).Where(l => l.Length > 0).ToArray();
|
||||
|
||||
[Test]
|
||||
public void TheCatalogEntriesAreTheMembersFile()
|
||||
{
|
||||
var actual = Wire.DocumentTypes.OrderBy(t => Wire.Registry.GetRequired(t).ToCatalogEntry().SchemaId, StringComparer.Ordinal)
|
||||
.SelectMany(t =>
|
||||
{
|
||||
var entry = Wire.Registry.GetRequired(t).ToCatalogEntry();
|
||||
return entry.Members.Select(m => $"{entry.SchemaId}\t{m.Slot}\t{m.MemberName}\t{m.TypeName}");
|
||||
}).ToArray();
|
||||
Assert.That(actual, Is.EqualTo(Lines("catalog-members.tsv")));
|
||||
Assert.That(actual, Has.Length.GreaterThan(20));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThePublicTypesAreTheTypeNamesFile()
|
||||
{
|
||||
var actual = typeof(CommandResult).Assembly.GetExportedTypes().Select(t => t.FullName!).OrderBy(n => n, StringComparer.Ordinal).ToArray();
|
||||
Assert.That(actual, Is.EqualTo(Lines("type-names.txt")));
|
||||
Assert.That(actual, Has.Length.GreaterThan(150));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AStoreWhoseRecordedTypeNameChangedIsRefusedOnReopen()
|
||||
{
|
||||
var scratch = Path.Combine(Path.GetTempPath(), $"thing-contract-names-{Guid.NewGuid():N}.cc");
|
||||
try
|
||||
{
|
||||
using (var cache = CultCacheMessagePack.Create(scratch, new CultCacheOpenOptions { Registry = Wire.Registry }))
|
||||
{
|
||||
Assert.That(cache.Commit(batch => batch.Upsert(Builders.FullAssetCatalog())), Is.True);
|
||||
cache.FlushAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
using (var cache = CultCacheMessagePack.Create(scratch, new CultCacheOpenOptions { Registry = Wire.Registry, ReadOnly = true }))
|
||||
Assert.That(cache.GetAll<AssetCatalog>().Single().Assets, Has.Count.EqualTo(2), "the unpatched store reopens");
|
||||
|
||||
var bytes = File.ReadAllBytes(scratch);
|
||||
var from = "System.Int64"u8.ToArray();
|
||||
var to = "System.Int32"u8.ToArray();
|
||||
var patched = 0;
|
||||
for (var i = 0; i + from.Length <= bytes.Length; i++)
|
||||
{
|
||||
if (!bytes.AsSpan(i, from.Length).SequenceEqual(from)) continue;
|
||||
to.CopyTo(bytes, i);
|
||||
patched++;
|
||||
}
|
||||
|
||||
Assert.That(patched, Is.GreaterThan(0), "the store records the type name");
|
||||
File.WriteAllBytes(scratch, bytes);
|
||||
var error = Assert.Catch(() =>
|
||||
{
|
||||
using var cache = CultCacheMessagePack.Create(scratch, new CultCacheOpenOptions { Registry = Wire.Registry, ReadOnly = true });
|
||||
})!;
|
||||
TestContext.Progress.WriteLine($"NAMEPATCH patched={patched} refused-by: {Wire.Flatten(error)}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(scratch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,5 +75,7 @@ public sealed class PresenceTests
|
|||
Assert.That(Assert.Throws<ThingDomainException>(() => ThingDomain.Items<string>(null, "Captures"))!.Domain, Is.EqualTo("Captures"));
|
||||
var nilItem = Assert.Throws<ThingDomainException>(() => ThingDomain.Items<string>(new List<string> { "a", null! }, "Captures"))!;
|
||||
Assert.That((nilItem.Domain, nilItem.Bound), Is.EqualTo(("Captures", "free of nil items")));
|
||||
var withCanary = Assert.Throws<ThingDomainException>(() => ThingDomain.Items<string>(new List<string> { Wire.Canary, null! }, "Captures"))!;
|
||||
Assert.That(withCanary.Message, Does.Not.Contain(Wire.Canary));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public sealed class ReadOnlyListTests
|
|||
}
|
||||
|
||||
Assert.That(offenders, Is.Empty);
|
||||
Assert.That(checkedProperties, Is.GreaterThan(300));
|
||||
Assert.That(checkedProperties, Is.GreaterThan(200));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
10
tests/thing-contract-parity/README.md
Normal file
10
tests/thing-contract-parity/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# thing-contract-parity
|
||||
|
||||
The names the C# package `GameCult.Thing.Contract` gives its types. The C# package owns them because CultLib's C# reference compares catalog type names when a store reopens, and every runtime package is held to the C# names.
|
||||
|
||||
| File | Rows |
|
||||
|---|---|
|
||||
| `catalog-members.tsv` | One row per document slot, `schemaId`, `slot`, `memberName`, `typeName`, as `Wire.Registry`'s catalog entries print them. A list slot reads `System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.X>`. |
|
||||
| `type-names.txt` | The full name of every public type in `GameCult.Thing.Contract`, sorted ordinally. |
|
||||
|
||||
`tests/GameCult.Thing.Contract.Tests/NameSetTests.cs` fails when either file disagrees with the C# types. These files change only in the commit that changes the C# types, and the other runtime packages read them without writing them.
|
||||
27
tests/thing-contract-parity/catalog-members.tsv
Normal file
27
tests/thing-contract-parity/catalog-members.tsv
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 0 InvocationId System.String
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 1 SurfaceKey System.String
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 2 SurfaceVersion System.Int64
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 3 Command System.String
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 4 Payload GameCult.Thing.Contract.RecordPayload
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 5 Captured System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.CapturedInput>
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 6 Client System.String
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 7 IssuedAt System.String
|
||||
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 8 Delegation System.String
|
||||
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 0 ProviderId System.String
|
||||
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 1 CatalogId System.String
|
||||
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 2 Version System.Int64
|
||||
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 3 Assets System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.AssetEntry>
|
||||
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 0 Publisher System.String
|
||||
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 1 Name System.String
|
||||
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 2 Version System.Int64
|
||||
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 3 Params System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.ParamDecl>
|
||||
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 4 Body GameCult.Thing.Contract.Node
|
||||
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 0 ProviderId System.String
|
||||
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 1 Label System.String
|
||||
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 2 Version System.Int64
|
||||
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 3 Title System.String
|
||||
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 4 Root GameCult.Thing.Contract.Node
|
||||
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 5 Commands System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.CommandDecl>
|
||||
sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 0 InvocationId System.String
|
||||
sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 1 Seq System.Int32
|
||||
sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 2 Outcome GameCult.Thing.Contract.Outcome
|
||||
|
168
tests/thing-contract-parity/type-names.txt
Normal file
168
tests/thing-contract-parity/type-names.txt
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
GameCult.Thing.Contract.AffixFormat
|
||||
GameCult.Thing.Contract.Anchor
|
||||
GameCult.Thing.Contract.AnchorNode
|
||||
GameCult.Thing.Contract.AnchorViewport
|
||||
GameCult.Thing.Contract.Arg
|
||||
GameCult.Thing.Contract.ArgValue
|
||||
GameCult.Thing.Contract.ArgValueBind
|
||||
GameCult.Thing.Contract.AssetCatalog
|
||||
GameCult.Thing.Contract.AssetEntry
|
||||
GameCult.Thing.Contract.Bind
|
||||
GameCult.Thing.Contract.BoolArg
|
||||
GameCult.Thing.Contract.BoolLiteral
|
||||
GameCult.Thing.Contract.BoolSource
|
||||
GameCult.Thing.Contract.BoolSourceBound
|
||||
GameCult.Thing.Contract.BoolSourceParam
|
||||
GameCult.Thing.Contract.BoolValue
|
||||
GameCult.Thing.Contract.Border
|
||||
GameCult.Thing.Contract.BoundOptions
|
||||
GameCult.Thing.Contract.BoxNode
|
||||
GameCult.Thing.Contract.BoxStyle
|
||||
GameCult.Thing.Contract.ButtonNode
|
||||
GameCult.Thing.Contract.CapturedInput
|
||||
GameCult.Thing.Contract.ChoiceValue
|
||||
GameCult.Thing.Contract.CollectionArg
|
||||
GameCult.Thing.Contract.CollectionSource
|
||||
GameCult.Thing.Contract.CollectionSourceMember
|
||||
GameCult.Thing.Contract.CollectionSourceParam
|
||||
GameCult.Thing.Contract.ColorRef
|
||||
GameCult.Thing.Contract.CommandArg
|
||||
GameCult.Thing.Contract.CommandDecl
|
||||
GameCult.Thing.Contract.CommandIdRef
|
||||
GameCult.Thing.Contract.CommandInvocation
|
||||
GameCult.Thing.Contract.CommandRef
|
||||
GameCult.Thing.Contract.CommandRefParam
|
||||
GameCult.Thing.Contract.CommandResult
|
||||
GameCult.Thing.Contract.CompletedOutcome
|
||||
GameCult.Thing.Contract.CompositionDocument
|
||||
GameCult.Thing.Contract.CompositionRef
|
||||
GameCult.Thing.Contract.ContentAlign
|
||||
GameCult.Thing.Contract.ContentAlignAuto
|
||||
GameCult.Thing.Contract.ContentAlignCenter
|
||||
GameCult.Thing.Contract.ContentAlignEnd
|
||||
GameCult.Thing.Contract.ContentAlignSpaceAround
|
||||
GameCult.Thing.Contract.ContentAlignSpaceBetween
|
||||
GameCult.Thing.Contract.ContentAlignSpaceEvenly
|
||||
GameCult.Thing.Contract.ContentAlignStart
|
||||
GameCult.Thing.Contract.ContentAlignStretch
|
||||
GameCult.Thing.Contract.DeniedOutcome
|
||||
GameCult.Thing.Contract.Dimension
|
||||
GameCult.Thing.Contract.DimensionAuto
|
||||
GameCult.Thing.Contract.DimensionContent
|
||||
GameCult.Thing.Contract.DimensionFr
|
||||
GameCult.Thing.Contract.DimensionPercent
|
||||
GameCult.Thing.Contract.DimensionPx
|
||||
GameCult.Thing.Contract.Direction
|
||||
GameCult.Thing.Contract.DirectionColumn
|
||||
GameCult.Thing.Contract.DirectionColumnReverse
|
||||
GameCult.Thing.Contract.DirectionRow
|
||||
GameCult.Thing.Contract.DirectionRowReverse
|
||||
GameCult.Thing.Contract.Display
|
||||
GameCult.Thing.Contract.DisplayBlock
|
||||
GameCult.Thing.Contract.DisplayFlex
|
||||
GameCult.Thing.Contract.DisplayGrid
|
||||
GameCult.Thing.Contract.Edges
|
||||
GameCult.Thing.Contract.Effect
|
||||
GameCult.Thing.Contract.EmbedNode
|
||||
GameCult.Thing.Contract.FailedOutcome
|
||||
GameCult.Thing.Contract.Format
|
||||
GameCult.Thing.Contract.FormatDuration
|
||||
GameCult.Thing.Contract.FormatTimestamp
|
||||
GameCult.Thing.Contract.ImageFit
|
||||
GameCult.Thing.Contract.ImageFitContain
|
||||
GameCult.Thing.Contract.ImageFitCover
|
||||
GameCult.Thing.Contract.ImageFitFill
|
||||
GameCult.Thing.Contract.ImageFitUnscaled
|
||||
GameCult.Thing.Contract.ImageNode
|
||||
GameCult.Thing.Contract.InputKind
|
||||
GameCult.Thing.Contract.InputKindNumber
|
||||
GameCult.Thing.Contract.InputKindText
|
||||
GameCult.Thing.Contract.InputValue
|
||||
GameCult.Thing.Contract.ItemAlign
|
||||
GameCult.Thing.Contract.ItemAlignAuto
|
||||
GameCult.Thing.Contract.ItemAlignBaseline
|
||||
GameCult.Thing.Contract.ItemAlignCenter
|
||||
GameCult.Thing.Contract.ItemAlignEnd
|
||||
GameCult.Thing.Contract.ItemAlignStart
|
||||
GameCult.Thing.Contract.ItemAlignStretch
|
||||
GameCult.Thing.Contract.LiteralOptions
|
||||
GameCult.Thing.Contract.MemberIndex
|
||||
GameCult.Thing.Contract.MemberName
|
||||
GameCult.Thing.Contract.MemberPath
|
||||
GameCult.Thing.Contract.MemberStep
|
||||
GameCult.Thing.Contract.Node
|
||||
GameCult.Thing.Contract.NodeCommon
|
||||
GameCult.Thing.Contract.NumberArg
|
||||
GameCult.Thing.Contract.NumberFormat
|
||||
GameCult.Thing.Contract.NumberLiteral
|
||||
GameCult.Thing.Contract.NumberSource
|
||||
GameCult.Thing.Contract.NumberSourceBound
|
||||
GameCult.Thing.Contract.NumberSourceParam
|
||||
GameCult.Thing.Contract.NumberValue
|
||||
GameCult.Thing.Contract.OpenExternalEffect
|
||||
GameCult.Thing.Contract.OpenSurfaceEffect
|
||||
GameCult.Thing.Contract.OptionList
|
||||
GameCult.Thing.Contract.Outcome
|
||||
GameCult.Thing.Contract.Overflow
|
||||
GameCult.Thing.Contract.OverflowClip
|
||||
GameCult.Thing.Contract.OverflowScroll
|
||||
GameCult.Thing.Contract.OverflowVisible
|
||||
GameCult.Thing.Contract.OverlayNode
|
||||
GameCult.Thing.Contract.Paint
|
||||
GameCult.Thing.Contract.ParamDecl
|
||||
GameCult.Thing.Contract.ParamKind
|
||||
GameCult.Thing.Contract.ParamKindBind
|
||||
GameCult.Thing.Contract.ParamKindBool
|
||||
GameCult.Thing.Contract.ParamKindCollection
|
||||
GameCult.Thing.Contract.ParamKindCommand
|
||||
GameCult.Thing.Contract.ParamKindNumber
|
||||
GameCult.Thing.Contract.ParamKindSlot
|
||||
GameCult.Thing.Contract.ParamKindText
|
||||
GameCult.Thing.Contract.PendingOutcome
|
||||
GameCult.Thing.Contract.PlotMark
|
||||
GameCult.Thing.Contract.PlotMarkArea
|
||||
GameCult.Thing.Contract.PlotMarkBar
|
||||
GameCult.Thing.Contract.PlotMarkLine
|
||||
GameCult.Thing.Contract.PlotNode
|
||||
GameCult.Thing.Contract.Position
|
||||
GameCult.Thing.Contract.PositionAbsolute
|
||||
GameCult.Thing.Contract.PositionRelative
|
||||
GameCult.Thing.Contract.RecordPayload
|
||||
GameCult.Thing.Contract.RecordsSource
|
||||
GameCult.Thing.Contract.RepeatNode
|
||||
GameCult.Thing.Contract.Rgba
|
||||
GameCult.Thing.Contract.Role
|
||||
GameCult.Thing.Contract.RoleDialog
|
||||
GameCult.Thing.Contract.RoleGroup
|
||||
GameCult.Thing.Contract.RoleHeading
|
||||
GameCult.Thing.Contract.RoleList
|
||||
GameCult.Thing.Contract.RoleListItem
|
||||
GameCult.Thing.Contract.RoleRegion
|
||||
GameCult.Thing.Contract.RoleToolbar
|
||||
GameCult.Thing.Contract.SelectNode
|
||||
GameCult.Thing.Contract.SelectOption
|
||||
GameCult.Thing.Contract.Size2
|
||||
GameCult.Thing.Contract.SliderNode
|
||||
GameCult.Thing.Contract.SlotArg
|
||||
GameCult.Thing.Contract.StateRef
|
||||
GameCult.Thing.Contract.StateSource
|
||||
GameCult.Thing.Contract.StateSourceItem
|
||||
GameCult.Thing.Contract.SurfaceDocument
|
||||
GameCult.Thing.Contract.TextArg
|
||||
GameCult.Thing.Contract.TextInputNode
|
||||
GameCult.Thing.Contract.TextLiteral
|
||||
GameCult.Thing.Contract.TextNode
|
||||
GameCult.Thing.Contract.TextSource
|
||||
GameCult.Thing.Contract.TextSourceBound
|
||||
GameCult.Thing.Contract.TextSourceParam
|
||||
GameCult.Thing.Contract.TextValue
|
||||
GameCult.Thing.Contract.ThemeToken
|
||||
GameCult.Thing.Contract.ThingDomain
|
||||
GameCult.Thing.Contract.ThingDomainException
|
||||
GameCult.Thing.Contract.ThingKeys
|
||||
GameCult.Thing.Contract.ToggleNode
|
||||
GameCult.Thing.Contract.UseNode
|
||||
GameCult.Thing.Contract.Wrap
|
||||
GameCult.Thing.Contract.WrapNoWrap
|
||||
GameCult.Thing.Contract.WrapWrap
|
||||
GameCult.Thing.Contract.WrapWrapReverse
|
||||
Loading…
Reference in a new issue