CultLib/tests/GameCult.Caching.Tests/UnkeyedDocumentMemberTests.cs
Meta Vens 9009aa79bc Generate derived documents' members and require [Key] on every one
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>
2026-09-13 21:19:08 +02:00

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()!;
}
}
}