diff --git a/tests/GameCult.Thing.Contract.Tests/Doors.cs b/tests/GameCult.Thing.Contract.Tests/Doors.cs index d536ce2..b67765c 100644 --- a/tests/GameCult.Thing.Contract.Tests/Doors.cs +++ b/tests/GameCult.Thing.Contract.Tests/Doors.cs @@ -35,7 +35,7 @@ internal static class Doors public static IReadOnlyList All() { var doors = new List(); - 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)); diff --git a/tests/GameCult.Thing.Contract.Tests/NameSetTests.cs b/tests/GameCult.Thing.Contract.Tests/NameSetTests.cs new file mode 100644 index 0000000..f987b44 --- /dev/null +++ b/tests/GameCult.Thing.Contract.Tests/NameSetTests.cs @@ -0,0 +1,77 @@ +using GameCult.Caching; +using GameCult.Caching.MessagePack; + +namespace GameCult.Thing.Contract.Tests; + +/// +/// 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. +/// +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().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); + } + } +} diff --git a/tests/GameCult.Thing.Contract.Tests/PresenceTests.cs b/tests/GameCult.Thing.Contract.Tests/PresenceTests.cs index 6f6bbc2..6f81e02 100644 --- a/tests/GameCult.Thing.Contract.Tests/PresenceTests.cs +++ b/tests/GameCult.Thing.Contract.Tests/PresenceTests.cs @@ -75,5 +75,7 @@ public sealed class PresenceTests Assert.That(Assert.Throws(() => ThingDomain.Items(null, "Captures"))!.Domain, Is.EqualTo("Captures")); var nilItem = Assert.Throws(() => ThingDomain.Items(new List { "a", null! }, "Captures"))!; Assert.That((nilItem.Domain, nilItem.Bound), Is.EqualTo(("Captures", "free of nil items"))); + var withCanary = Assert.Throws(() => ThingDomain.Items(new List { Wire.Canary, null! }, "Captures"))!; + Assert.That(withCanary.Message, Does.Not.Contain(Wire.Canary)); } } diff --git a/tests/GameCult.Thing.Contract.Tests/ReadOnlyListTests.cs b/tests/GameCult.Thing.Contract.Tests/ReadOnlyListTests.cs index 68b37e6..a0fe12e 100644 --- a/tests/GameCult.Thing.Contract.Tests/ReadOnlyListTests.cs +++ b/tests/GameCult.Thing.Contract.Tests/ReadOnlyListTests.cs @@ -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] diff --git a/tests/thing-contract-parity/README.md b/tests/thing-contract-parity/README.md new file mode 100644 index 0000000..87895a0 --- /dev/null +++ b/tests/thing-contract-parity/README.md @@ -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`. | +| `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. diff --git a/tests/thing-contract-parity/catalog-members.tsv b/tests/thing-contract-parity/catalog-members.tsv new file mode 100644 index 0000000..2f449fb --- /dev/null +++ b/tests/thing-contract-parity/catalog-members.tsv @@ -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 +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 +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 +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 +sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 0 InvocationId System.String +sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 1 Seq System.Int32 +sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 2 Outcome GameCult.Thing.Contract.Outcome diff --git a/tests/thing-contract-parity/type-names.txt b/tests/thing-contract-parity/type-names.txt new file mode 100644 index 0000000..fa312ec --- /dev/null +++ b/tests/thing-contract-parity/type-names.txt @@ -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