The generator walked only members declared on the document class and emitted
a codec only when those were dense, so a derived document's generated
descriptor hashed a different schema id and catalog than the reflective one.
It now discovers members along the base-type chain the way
CultDocumentRegistry reflects them (public instance fields and get/set
properties, inherited included, [IgnoreMember] excluded), and density is
over that full set: RoutingWeapon : RoutingGear gets a codec and the same
SchemaId and catalog (Name@0, Damage@1) from both builds.
Unkeyed members made the two orderings disagree (generator by name, registry
by metadata token). Both implicit-slot paths are deleted: an unkeyed
persisted member of a [CultDocument] type is generator error GCC001, and the
registry throws the same message. The consumer audit found no unkeyed
document with a persisted store.
Tests: GeneratedAndReflectiveDescriptorsAgreeForDerivedDocuments failed on
ea5a07b ("no generated codec for the derived document") and passes through
UpsertWeaponThroughGearHandleReloadsAsWeapon; UnkeyedDocumentMemberIsRejected
drives the generator through Roslyn and compares its diagnostic with the
registry's exception. The registry's reflective BuildDescriptor is internal
and visible to the tests for that comparison. The registry-collision fixture's
emitted field gains its [Key]; schema-compatibility fixture ids are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.9 KiB
C#
66 lines
2.9 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
using GameCult.Caching;
|
|
using GameCult.Caching.MessagePack.Generator;
|
|
using MessagePack;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using NUnit.Framework;
|
|
|
|
namespace GameCult.Caching.Tests
|
|
{
|
|
public class UnkeyedDocumentMemberTests
|
|
{
|
|
[Test]
|
|
public void UnkeyedDocumentMemberIsRejected()
|
|
{
|
|
var reflective = Assert.Throws<InvalidOperationException>(() => CultDocumentRegistry.ForTypes(new[] { EmitUnkeyedDocument() }))!;
|
|
|
|
const string source = @"
|
|
using GameCult.Caching;
|
|
using MessagePack;
|
|
[CultDocument(""tests.unkeyed"", ""tests.unkeyed.v1"")]
|
|
public class UnkeyedDocument
|
|
{
|
|
[Key(0)] public string Name = string.Empty;
|
|
public int Count;
|
|
}";
|
|
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
|
|
.Split(Path.PathSeparator)
|
|
.Append(typeof(CultDocumentAttribute).Assembly.Location)
|
|
.Append(typeof(KeyAttribute).Assembly.Location)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.Select(path => MetadataReference.CreateFromFile(path));
|
|
var compilation = CSharpCompilation.Create(
|
|
"UnkeyedConsumer",
|
|
new[] { CSharpSyntaxTree.ParseText(source) },
|
|
references,
|
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
|
CSharpGeneratorDriver.Create(new CultDocumentMessagePackGenerator())
|
|
.RunGeneratorsAndUpdateCompilation(compilation, out _, out var diagnostics);
|
|
|
|
var generated = diagnostics.Single(diagnostic => diagnostic.Id == "GCC001");
|
|
Assert.That(generated.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
|
Assert.That(generated.GetMessage(), Is.EqualTo(reflective.Message));
|
|
Assert.That(reflective.Message, Does.Contain("UnkeyedDocument member Count has no [Key]"));
|
|
}
|
|
|
|
private static Type EmitUnkeyedDocument()
|
|
{
|
|
var module = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("UnkeyedDocuments"), AssemblyBuilderAccess.Run)
|
|
.DefineDynamicModule("UnkeyedDocuments");
|
|
var type = module.DefineType("UnkeyedDocument", TypeAttributes.Public | TypeAttributes.Class);
|
|
type.SetCustomAttribute(new CustomAttributeBuilder(
|
|
typeof(CultDocumentAttribute).GetConstructor(new[] { typeof(string), typeof(string) })!,
|
|
new object[] { "tests.unkeyed", "tests.unkeyed.v1" }));
|
|
type.DefineField("Name", typeof(string), FieldAttributes.Public)
|
|
.SetCustomAttribute(new CustomAttributeBuilder(typeof(KeyAttribute).GetConstructor(new[] { typeof(int) })!, new object[] { 0 }));
|
|
type.DefineField("Count", typeof(int), FieldAttributes.Public);
|
|
return type.CreateType()!;
|
|
}
|
|
}
|
|
}
|