CultLib/tests/GameCult.Networking.Tests/CultNetDocumentBindingResolverTests.cs

68 lines
2.4 KiB
C#
Raw Normal View History

Make generated and reflective document members one rule The generator and CultDocumentRegistry now discover members by the same steps: most-derived declaration first, override chain collapsed to one member, attributes read up the override chain, rejections in one order with one text (GCC001 / InvalidOperationException). - Accessors are named per document, so documents sharing a [CultName] or [CultIndex] base member no longer emit duplicate methods (CS0111). - Duplicate [Key] slots are refused, including new-hidden fields and properties; a hidden persisted member with a different slot is refused. - String [Key("x")] gets its own message instead of "has no [Key]". - A persisted property whose set accessor is private, protected or init, or a readonly field, is refused instead of CS0272 in generated code. - Overrides: MessagePack's DynamicObjectResolver reads the base declaration's [Key]/[IgnoreMember] (it throws "key is duplicated" for a re-keyed override and serializes an override marked [IgnoreMember]). An override that changes them is refused in both paths; an override that repeats or omits them is accepted and byte-pinned. - Typed Serialize<T>/Deserialize<T> used static Options and skipped generated codecs and declared resolvers; deleted. CultNetDocumentBinding and CultMeshSnapshots route through the untyped path with their registry. DocumentShapeSweepTests compiles each shape through the generator and compares generated vs reflective schema id, catalog, name/index values and payload against MessagePack's own bytes, or equal rejection text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 22:00:16 +02:00
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Networking;
using MessagePack;
using MessagePack.Formatters;
using NUnit.Framework;
[assembly: CultCacheFormatterResolver(typeof(GameCult.Networking.Tests.BindingPairResolver))]
namespace GameCult.Networking.Tests
{
public class CultNetDocumentBindingResolverTests
{
[Test]
public void TypedBindingUsesTheDocumentAssemblyResolver()
{
var registry = CultDocumentRegistry.ForTypes(new[] { typeof(BindingPairDocument) });
var binding = CultNetDocumentBinding.ForDocument<BindingPairDocument>(registry);
var payload = binding.PayloadSerializer(new BindingPairDocument { Name = "p", Value = new BindingPair { A = 1.5f, B = -2f } });
var decoded = (BindingPairDocument)binding.PayloadDeserializer(payload);
Assert.That(decoded.Name, Is.EqualTo("p"));
Assert.That(decoded.Value.A, Is.EqualTo(1.5f));
Assert.That(decoded.Value.B, Is.EqualTo(-2f));
}
}
public struct BindingPair
{
public float A;
public float B;
}
[CultDocument("tests.binding_pair", "tests.binding_pair.v1")]
[MessagePackObject]
public sealed class BindingPairDocument
{
[Key(0)] [CultName] public string Name = string.Empty;
[Key(1)] public BindingPair Value;
}
public sealed class BindingPairResolver : IFormatterResolver
{
public static readonly IFormatterResolver Instance = new BindingPairResolver();
public IMessagePackFormatter<T>? GetFormatter<T>() =>
typeof(T) == typeof(BindingPair) ? (IMessagePackFormatter<T>)(object)PairFormatter.Instance : null;
private sealed class PairFormatter : IMessagePackFormatter<BindingPair>
{
public static readonly PairFormatter Instance = new();
public void Serialize(ref MessagePackWriter writer, BindingPair value, MessagePackSerializerOptions options)
{
writer.WriteArrayHeader(2);
writer.Write(value.A);
writer.Write(value.B);
}
public BindingPair Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
reader.ReadArrayHeader();
return new BindingPair { A = reader.ReadSingle(), B = reader.ReadSingle() };
}
}
}
}