CultLib/tests/GameCult.Caching.Tests/CultDocumentRegistryTests.cs
Meta Vens 96ee5fdce3 Delete the CultCache MessagePack source generator
The reflective CultDocumentRegistry is now the only descriptor authority,
and payloads always go through MessagePackSerializer with
OptionsFor(document assembly). No consumer builds AOT, IL2CPP or trimmed,
Unity always ran the reflective path, and no benchmark showed a gain,
while the generator duplicated the registry's shape rules.

Removed: GameCult.Caching.MessagePack.Generator and .Analyzers (solution,
project and analyzer references, NuGet pack list), CultGeneratedDocumentMetadata
(provider attribute, interface, definitions, loader), the generated descriptor
and codec branches in CultCache and CultDocumentMessagePackSerialization,
DocumentShapeSweepTests and the generator half of the unkeyed test, the
Microsoft.CodeAnalysis.CSharp test reference and InternalsVisibleTo.

The registry rejects, with a message naming the fix, what MessagePackSerializer
cannot serialize and what the generator used to paper over: a document without
[MessagePackObject], a non-public document without AllowPrivate = true, and a
non-public member of an AllowPrivate document that is not [IgnoreMember].
DocumentShapeTests pins every rejection message and the accepted shapes'
round trips against MessagePackSerializer bytes (keeps the 93A16E0305 pin).
Rejected shapes are emitted into a dynamic assembly, which registry
discovery skips.

Nested member type names use reflection's Outer+Inner form, not the
generator's dot form, so ReferenceHolderEntry's fixture id is re-pinned
(bd8506... -> 4ec0c5...). A scan of the schema catalogs in all 2786 .cc
stores under F:\Projects (AetheriaEve and CultLib-* worktrees, bin and obj
excluded) found no member type name naming a nested type in either form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:38:08 +02:00

155 lines
6.3 KiB
C#

using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading.Tasks;
using NUnit.Framework;
namespace GameCult.Caching.Tests;
[TestFixture]
public sealed class CultDocumentRegistryTests
{
[Test]
public void ForTypesContainsOnlyExplicitDocuments()
{
var requested = CreateDocumentType("explicit", "tests.registry.explicit", "v1");
var unrelated = CreateDocumentType("unrelated", "tests.registry.unrelated", "v1");
var registry = CultDocumentRegistry.ForTypes(new[] { requested, requested });
Assert.That(registry.AllDescriptors.Select(value => value.DocumentType), Is.EqualTo(new[] { requested }));
Assert.That(registry.AllDescriptors.Any(value => value.DocumentType == unrelated), Is.False);
}
[Test]
public void IdenticalTypeRegistrationIsIdempotent()
{
var registry = new CultDocumentRegistry();
var type = CreateDocumentType("idempotent", "tests.registry.idempotent", "v1");
var first = registry.GetRequired(type);
var second = registry.GetRequired(type);
Assert.That(second, Is.SameAs(first));
}
[Test]
public void ExactWireCompatibleAliasRegistersByTypeWithOneCanonicalSchemaDescriptor()
{
var registry = new CultDocumentRegistry();
var firstType = CreateDocumentType("alias_owner", "tests.registry.alias", "v1");
var aliasType = CreateDocumentType("alias_claimant", "tests.registry.alias", "v1");
var canonical = registry.GetRequired(firstType);
var alias = registry.GetRequired(aliasType);
Assert.Multiple(() =>
{
Assert.That(alias.DocumentType, Is.EqualTo(aliasType));
Assert.That(alias.SchemaId, Is.EqualTo(canonical.SchemaId));
Assert.That(registry.GetRequired(aliasType), Is.SameAs(alias));
Assert.That(registry.GetRequiredBySchemaId(canonical.SchemaId), Is.SameAs(canonical));
Assert.That(
registry.AllDescriptors.Count(descriptor => descriptor.SchemaId == canonical.SchemaId),
Is.EqualTo(2));
});
}
[Test]
public void DifferentVersionsSharingSchemaNameRegisterWithoutReflectionOrderSelection()
{
var registry = new CultDocumentRegistry();
var firstType = CreateDocumentType("version_one", "tests.registry.versioned", "v1");
var secondType = CreateDocumentType("version_two", "tests.registry.versioned", "v2");
var first = registry.GetRequired(firstType);
var second = registry.GetRequired(secondType);
Assert.Multiple(() =>
{
Assert.That(registry.GetRequiredBySchemaId(first.SchemaId), Is.SameAs(first));
Assert.That(registry.GetRequiredBySchemaId(second.SchemaId), Is.SameAs(second));
});
var persisted = new CultSchemaCatalogEntry
{
SchemaId = "tests.registry.versioned.unknown",
SchemaName = "tests.registry.versioned",
SchemaVersion = "legacy"
};
Assert.That(
() => registry.ResolvePersistedSchema(persisted.SchemaId, new[] { persisted }),
Throws.TypeOf<InvalidOperationException>()
.With.Message.Contains("ambiguous across local versions")
.And.Message.Contains("v1")
.And.Message.Contains("v2"));
}
[Test]
public void IncompatibleTypesClaimingSameSchemaNameAndVersionFailBeforeMutation()
{
var registry = new CultDocumentRegistry();
var firstType = CreateDocumentType("layout_owner", "tests.registry.layout_collision", "v1");
var secondType = CreateDocumentType(
"layout_claimant",
"tests.registry.layout_collision",
"v1",
addStringField: true);
var first = registry.GetRequired(firstType);
Assert.That(
() => registry.GetRequired(secondType),
Throws.TypeOf<InvalidOperationException>()
.With.Message.Contains("schema name 'tests.registry.layout_collision' version 'v1'")
.And.Message.Contains(firstType.FullName)
.And.Message.Contains(secondType.FullName));
Assert.That(registry.GetRequiredBySchemaId(first.SchemaId), Is.SameAs(first));
Assert.That(registry.AllDescriptors.Any(descriptor => descriptor.DocumentType == secondType), Is.False);
}
[Test]
public void ConcurrentRefreshAndReadsObserveCompleteRegistrySnapshots()
{
var registry = new CultDocumentRegistry();
var type = CreateDocumentType("refresh_visible", "tests.registry.refresh_visible", "v1");
var descriptor = registry.GetRequired(type);
Parallel.For(0, 20, _ =>
{
registry.Refresh();
Assert.That(registry.GetRequired(type).SchemaId, Is.EqualTo(descriptor.SchemaId));
Assert.That(registry.GetRequiredBySchemaId(descriptor.SchemaId).SchemaId, Is.EqualTo(descriptor.SchemaId));
});
}
private static Type CreateDocumentType(
string typeName,
string schemaName,
string schemaVersion,
bool addStringField = false)
{
var assembly = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("GameCult.Caching.RegistryTests." + Guid.NewGuid().ToString("N")),
AssemblyBuilderAccess.Run);
var module = assembly.DefineDynamicModule("main");
var type = module.DefineType(
typeName,
TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed);
var attributeConstructor = typeof(CultDocumentAttribute).GetConstructor(new[] { typeof(string), typeof(string) })!;
type.SetCustomAttribute(new CustomAttributeBuilder(
attributeConstructor,
new object[] { schemaName, schemaVersion }));
type.SetCustomAttribute(new CustomAttributeBuilder(
typeof(global::MessagePack.MessagePackObjectAttribute).GetConstructor(new[] { typeof(bool) })!,
new object[] { false }));
if (addStringField)
{
type.DefineField("Value", typeof(string), FieldAttributes.Public).SetCustomAttribute(new CustomAttributeBuilder(
typeof(global::MessagePack.KeyAttribute).GetConstructor(new[] { typeof(int) })!,
new object[] { 0 }));
}
return type.CreateType()!;
}
}