diff --git a/_build/_build.csproj b/_build/_build.csproj index 937be89e..349c539f 100644 --- a/_build/_build.csproj +++ b/_build/_build.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 $(NoWarn);CS0649;CS0169;CS1591;CS1573 disable diff --git a/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj b/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj index 5fd943a2..426457ba 100644 --- a/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj +++ b/benchmarks/Backdash.Benchmarks.Ping/Backdash.Benchmarks.Ping.csproj @@ -1,7 +1,7 @@ - + Exe - net8.0 + net9.0 enable enable CS1591,S125,S1199,CS016,S1144,S3903,CS159,S1104 diff --git a/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj b/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj index 8960fbea..94131335 100644 --- a/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj +++ b/benchmarks/Backdash.Benchmarks/Backdash.Benchmarks.csproj @@ -1,7 +1,7 @@ - + Exe - net8.0 + net9.0 enable enable CS1591;S125;S1199;CS016;S1144;S3903;CS159;S1104;MSB3277 diff --git a/global.json b/global.json index d47cb3fb..aeceea9d 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.404", + "version": "9.0.101", "rollForward": "latestMinor", "allowPrerelease": false } diff --git a/src/Backdash.Analyzers/Backdash.Analyzers.csproj b/src/Backdash.Analyzers/Backdash.Analyzers.csproj index d77fc889..c66946e5 100644 --- a/src/Backdash.Analyzers/Backdash.Analyzers.csproj +++ b/src/Backdash.Analyzers/Backdash.Analyzers.csproj @@ -1,6 +1,7 @@ - + - net8.0 + netstandard2.0 + preview true diff --git a/src/Backdash.Analyzers/IsExternalInit.cs b/src/Backdash.Analyzers/IsExternalInit.cs new file mode 100644 index 00000000..ded2fabc --- /dev/null +++ b/src/Backdash.Analyzers/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices +{ + /// + /// This is needed to make the analyzers work in .netstandard2.0 (so they are compatible with visual studio) + /// + internal static class IsExternalInit { } +} diff --git a/src/Backdash.Analyzers/SourceGenerationHelper.cs b/src/Backdash.Analyzers/SourceGenerationHelper.cs index c50c1139..fcbc1e17 100644 --- a/src/Backdash.Analyzers/SourceGenerationHelper.cs +++ b/src/Backdash.Analyzers/SourceGenerationHelper.cs @@ -189,7 +189,7 @@ static bool IsTypeArrayCopiable(ITypeSymbol type) { Debug.Assert(type != null); - if (!type.IsUnmanagedType || type is INamedTypeSymbol { EnumUnderlyingType: not null }) + if (!(type?.IsUnmanagedType ?? false) || type is INamedTypeSymbol { EnumUnderlyingType: not null }) return false; return type.SpecialType switch diff --git a/src/Backdash.Utils/Backdash.Utils.csproj b/src/Backdash.Utils/Backdash.Utils.csproj index 66fdbfdf..97cd3b3e 100644 --- a/src/Backdash.Utils/Backdash.Utils.csproj +++ b/src/Backdash.Utils/Backdash.Utils.csproj @@ -1,7 +1,7 @@ - + - net8.0 + net9.0 enable true Backdash @@ -9,6 +9,7 @@ Backdash network utilities network, endpoint, multiplayer, json, input CS1591 + true diff --git a/src/Backdash/Backdash.csproj b/src/Backdash/Backdash.csproj index 28da869d..a0c35268 100644 --- a/src/Backdash/Backdash.csproj +++ b/src/Backdash/Backdash.csproj @@ -1,11 +1,12 @@ - + - net8.0 + net9.0 enable true Backdash Rollback netcode library rollback, netcode, network, peer to peer, online, game, multiplayer + true @@ -21,7 +22,7 @@ - + diff --git a/src/Backdash/Backends/BackendServices.cs b/src/Backdash/Backends/BackendServices.cs index 7257cd58..f03e26a4 100644 --- a/src/Backdash/Backends/BackendServices.cs +++ b/src/Backdash/Backends/BackendServices.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Backdash.Core; using Backdash.Network; using Backdash.Network.Client; @@ -11,7 +12,7 @@ namespace Backdash.Backends; -sealed class BackendServices +sealed class BackendServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput, TGameState> where TInput : unmanaged where TGameState : notnull, new() { @@ -56,7 +57,7 @@ public BackendServices(RollbackOptions options, SessionServices Create( + public static BackendServices Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput, TGameState>( RollbackOptions options, SessionServices? services ) diff --git a/src/Backdash/Backends/SyncTestBackend.cs b/src/Backdash/Backends/SyncTestBackend.cs index 941168a8..6f476633 100644 --- a/src/Backdash/Backends/SyncTestBackend.cs +++ b/src/Backdash/Backends/SyncTestBackend.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Backdash.Core; using Backdash.Data; using Backdash.Network; @@ -39,6 +41,9 @@ GameInput Input IncludeFields = true, }; + readonly JsonTypeInfo? gameStateJsonTypeInfo; + readonly JsonTypeInfo? inputJsonTypeInfo; + IRollbackHandler callbacks; bool inRollback; bool running; @@ -47,6 +52,8 @@ GameInput Input GameInput lastInput; Frame lastVerified = Frame.Zero; + [RequiresUnreferencedCode("Use the constructor which provides JsonTypeInfo(s) instead")] + [RequiresDynamicCode("Use the constructor which provides JsonTypeInfo(s) instead")] public SyncTestBackend( RollbackOptions options, FrameSpan checkDistance, @@ -78,6 +85,29 @@ BackendServices services }; currentInput = new(); lastInput = new(); + jsonOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "gameStateJsonTypeInfo and inputJsonTypeInfo are set.")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "gameStateJsonTypeInfo and inputJsonTypeInfo are set.")] + public SyncTestBackend( + RollbackOptions options, + FrameSpan checkDistance, + bool throwError, + BackendServices services, + JsonTypeInfo gameStateJsonTypeInfo, + JsonTypeInfo inputJsonTypeInfo + ) : this( + options, + checkDistance, + throwError, + services + ) + { + ArgumentNullException.ThrowIfNull(gameStateJsonTypeInfo); + ArgumentNullException.ThrowIfNull(inputJsonTypeInfo); + this.gameStateJsonTypeInfo = gameStateJsonTypeInfo; + this.inputJsonTypeInfo = inputJsonTypeInfo; } public void Dispose() => tsc.SetResult(); @@ -210,11 +240,7 @@ public void AdvanceFrame() savedFrames.Enqueue(new( Frame: frame, Input: lastInput, -#if AOT_ENABLED - State: lastSaved.GameState.ToString() ?? string.Empty, -#else - State: JsonSerializer.Serialize(lastSaved.GameState, jsonOptions), -#endif + State: JsonSerializer.Serialize(lastSaved.GameState, gameStateJsonTypeInfo ?? jsonOptions.TypeInfoResolver?.GetTypeInfo(typeof(TGameState), jsonOptions) ?? throw new InvalidOperationException("Could not get JsonTypeInfo")), Checksum: lastSaved.Checksum )); if (frame - lastVerified != checkDistance.FrameValue) @@ -259,13 +285,9 @@ void LogSaveState(SavedFrame info, string description) const LogLevel level = LogLevel.Information; logger.Write(level, $"=== SAVED STATE [{description.ToUpper()}] ({info.Frame}) ===\n"); logger.Write(level, $"INPUT FRAME {info.Input.Frame}:"); -#if AOT_ENABLED - logger.Write(level, info.Input.Data.ToString() ?? string.Empty); -#else - logger.Write(level, JsonSerializer.Serialize(info.Input.Data, jsonOptions)); -#endif + logger.Write(level, JsonSerializer.Serialize(info.Input.Data, inputJsonTypeInfo ?? jsonOptions.TypeInfoResolver?.GetTypeInfo(typeof(TInput), jsonOptions) as JsonTypeInfo ?? throw new InvalidOperationException("Could not get JsonTypeInfo"))); logger.Write(level, $"GAME STATE #{info.Checksum}:"); - LogJson(level, info.State); + LogStringChunked(level, info.State); logger.Write(level, "===================================="); } @@ -274,18 +296,23 @@ void LogSaveState(SavedFrame info, string description) const LogLevel level = LogLevel.Information; logger.Write(level, $"=== SAVED STATE [{description.ToUpper()}] ({info.Frame}) ===\n"); logger.Write(level, $"GAME STATE #{info.Checksum}:"); - LogJson(level, info.GameState); + LogJson(level, info.GameState, gameStateJsonTypeInfo ?? jsonOptions.TypeInfoResolver?.GetTypeInfo(typeof(TGameState), jsonOptions) as JsonTypeInfo ?? throw new InvalidOperationException("Could not get JsonTypeInfo")); logger.Write(level, "===================================="); } - void LogJson(LogLevel level, TValue value) + void LogStringChunked(LogLevel level, string value) + { + var chunks = value + .Chunk(LogStringBuffer.Capacity / 2) + .Select(x => new string(x)); + foreach (var chunk in chunks) + logger.Write(level, chunk); + } + + void LogJson(LogLevel level, TValue value, JsonTypeInfo jsonTypeInfo) { var jsonChunks = -#if AOT_ENABLED - (value?.ToString() ?? string.Empty) -#else - JsonSerializer.Serialize(value, jsonOptions) -#endif + JsonSerializer.Serialize(value, jsonTypeInfo) .Chunk(LogStringBuffer.Capacity / 2) .Select(x => new string(x)); foreach (var chunk in jsonChunks) diff --git a/src/Backdash/Core/LogInterpolatedStringHandler.cs b/src/Backdash/Core/LogInterpolatedStringHandler.cs index 3caee2d7..93b4fafb 100644 --- a/src/Backdash/Core/LogInterpolatedStringHandler.cs +++ b/src/Backdash/Core/LogInterpolatedStringHandler.cs @@ -124,4 +124,17 @@ struct LogStringBuffer #endif byte elemenet0; + + /// + public override readonly int GetHashCode() => Mem.GetHashCode(this); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public readonly bool Equals(LogStringBuffer other) => this[..].SequenceEqual(other); + + /// + public override readonly bool Equals(object? obj) => obj is LogStringBuffer other && Equals(other); } diff --git a/src/Backdash/Core/TypeHelpers.cs b/src/Backdash/Core/TypeHelpers.cs index e34db1f5..391ba266 100644 --- a/src/Backdash/Core/TypeHelpers.cs +++ b/src/Backdash/Core/TypeHelpers.cs @@ -1,27 +1,37 @@ -#if !AOT_ENABLED +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Backdash.Core; static class TypeHelpers { - public static T? Instantiate(bool allowPrivateConstructor = true) where T : notnull + public static T? Instantiate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(bool allowPrivateConstructor = true) where T : notnull { var type = typeof(T); if (type.IsValueType) return default; var flags = BindingFlags.Instance | BindingFlags.Public; - if (allowPrivateConstructor) -#pragma warning disable S3011 - flags |= BindingFlags.NonPublic; -#pragma warning restore S3011 var ctor = type.GetConstructor(flags, null, Type.EmptyTypes, null); if (ctor is not null) return (T)ctor.Invoke([]); return default; } - public static bool HasInvariantHashCode() where T : notnull + public static T? InstantiateWithNonPublicConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(bool allowPrivateConstructor = true) where T : notnull + { + var type = typeof(T); + if (type.IsValueType) + return default; +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + var ctor = type.GetConstructor(flags, null, Type.EmptyTypes, null); + if (ctor is not null) + return (T)ctor.Invoke([]); + return default; + } + + public static bool HasInvariantHashCode<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>() where T : notnull { var comparer = EqualityComparer.Default; return Instantiate() is { } state1 @@ -31,4 +41,3 @@ public static bool HasInvariantHashCode() where T : notnull && comparer.GetHashCode(state1) == comparer.GetHashCode(state3); } } -#endif diff --git a/src/Backdash/Network/Client/PeerClientFactory.cs b/src/Backdash/Network/Client/PeerClientFactory.cs index ecd115e8..b4bc43f5 100644 --- a/src/Backdash/Network/Client/PeerClientFactory.cs +++ b/src/Backdash/Network/Client/PeerClientFactory.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Backdash.Core; using Backdash.Serialization; @@ -30,11 +31,11 @@ public static IPeerClient Create( maxPacketSize ); -#if !AOT_ENABLED /// /// Creates new /// - public static IPeerClient Create( + /// Prefer using the overload in NativeAoT/Trimmed applications + public static IPeerClient Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>( IPeerSocket socket, IPeerObserver observer, int maxPacketSize = Max.UdpPacketSize, @@ -52,5 +53,4 @@ public static IPeerClient Create( delayStrategy, random ); -#endif } diff --git a/src/Backdash/RollbackNetcode.cs b/src/Backdash/RollbackNetcode.cs index 00a02886..02414914 100644 --- a/src/Backdash/RollbackNetcode.cs +++ b/src/Backdash/RollbackNetcode.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Text.Json.Serialization.Metadata; using Backdash.Backends; using Backdash.Core; using Backdash.Data; @@ -23,7 +25,7 @@ public static class RollbackNetcode /// Session customizable dependencies /// Game input type /// Game state type - public static IRollbackSession CreateSession( + public static IRollbackSession CreateSession<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput, TGameState>( int port, RollbackOptions? options = null, SessionServices? services = null @@ -45,7 +47,7 @@ public static IRollbackSession CreateSessionSession customizable dependencies /// Game input type /// Game state type - public static IRollbackSession CreateSpectatorSession( + public static IRollbackSession CreateSpectatorSession<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput, TGameState>( int port, IPEndPoint host, int numberOfPlayers, @@ -70,7 +72,7 @@ public static IRollbackSession CreateSpectatorSession /// Game input type /// Game state type - public static IRollbackSession CreateReplaySession( + public static IRollbackSession CreateReplaySession<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput, TGameState>( int numberOfPlayers, IReadOnlyList> inputs, SessionServices? services = null, @@ -92,6 +94,8 @@ public static IRollbackSession CreateReplaySessionIf true, throws on state de-synchronization. /// Game input type /// Game state type + [RequiresUnreferencedCode("Use the CreateSyncTestSession overload which provides JsonTypeInfo(s) instead")] + [RequiresDynamicCode("Use the CreateSyncTestSession overload which provides JsonTypeInfo(s) instead")] public static IRollbackSession CreateSyncTestSession( FrameSpan? checkDistance = null, RollbackOptions? options = null, @@ -112,4 +116,40 @@ public static IRollbackSession CreateSyncTestSession + /// Initializes new sync test session. + /// + /// json serialization information + /// json serialization information + /// Total forced rollback frames. + /// Session configuration + /// Session customizable dependencies + /// If true, throws on state de-synchronization. + /// Game input type + /// Game state type + public static IRollbackSession CreateSyncTestSession<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput, TGameState>( + JsonTypeInfo gameStateJsonTypeInfo, + JsonTypeInfo inputJsonTypeInfo, + FrameSpan? checkDistance = null, + RollbackOptions? options = null, + SessionServices? services = null, + bool throwException = true + ) + where TInput : unmanaged + where TGameState : notnull, new() + { + options ??= new() + { + // ReSharper disable once RedundantArgumentDefaultValue + Log = new(LogLevel.Information), + }; + checkDistance ??= FrameSpan.One; + return new SyncTestBackend( + options, checkDistance.Value, throwException, + BackendServices.Create(options, services), + gameStateJsonTypeInfo, + inputJsonTypeInfo + ); + } } diff --git a/src/Backdash/Serialization/BinarySerializerFactory.cs b/src/Backdash/Serialization/BinarySerializerFactory.cs index 9a625e04..13ab981f 100644 --- a/src/Backdash/Serialization/BinarySerializerFactory.cs +++ b/src/Backdash/Serialization/BinarySerializerFactory.cs @@ -1,10 +1,8 @@ using System.Numerics; using Backdash.Core; using Backdash.Network; - -#if !AOT_ENABLED +using System.Diagnostics.CodeAnalysis; using System.Reflection; -#endif namespace Backdash.Serialization; @@ -39,12 +37,10 @@ public static IBinarySerializer ForStruct() where TInput : struc return new StructBinarySerializer(); } - public static IBinarySerializer? Get(bool networkEndianness = true) + + public static IBinarySerializer? Get<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput>(bool networkEndianness = true) where TInput : unmanaged { -#if AOT_ENABLED - return null; -#else var inputType = typeof(TInput); Type[] integerInterfaces = [typeof(IBinaryInteger<>), typeof(IMinMaxValue<>)]; return inputType switch @@ -73,10 +69,9 @@ public static IBinarySerializer ForStruct() where TInput : struc .Invoke(null, []) as IBinarySerializer, _ => null, }; -#endif } - public static IBinarySerializer FindOrThrow(bool networkEndianness = true) + public static IBinarySerializer FindOrThrow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TInput>(bool networkEndianness = true) where TInput : unmanaged => Get(networkEndianness) ?? throw new InvalidOperationException($"Unable to infer serializer for type {typeof(TInput).FullName}"); diff --git a/src/Backdash/Synchronizing/Input/Confirmed/ConfirmedInputs.cs b/src/Backdash/Synchronizing/Input/Confirmed/ConfirmedInputs.cs index f99c874a..74d6cc6f 100644 --- a/src/Backdash/Synchronizing/Input/Confirmed/ConfirmedInputs.cs +++ b/src/Backdash/Synchronizing/Input/Confirmed/ConfirmedInputs.cs @@ -48,4 +48,17 @@ public struct InputArray where TInput : unmanaged public const int Capacity = Max.NumberOfPlayers; TInput element0; + + /// + public override readonly int GetHashCode() => Mem.GetHashCode(this); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public readonly bool Equals(InputArray other) => this[..].SequenceEqual(other); + + /// + public override readonly bool Equals(object? obj) => obj is InputArray other && Equals(other); } diff --git a/src/Backdash/Synchronizing/State/ChecksumProvider.cs b/src/Backdash/Synchronizing/State/ChecksumProvider.cs index bb2cb392..c6018cc1 100644 --- a/src/Backdash/Synchronizing/State/ChecksumProvider.cs +++ b/src/Backdash/Synchronizing/State/ChecksumProvider.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace Backdash.Synchronizing.State; /// @@ -33,12 +35,10 @@ sealed class EmptyChecksumProvider : IChecksumProvider where T : notnull static class ChecksumProviderFactory { - public static IChecksumProvider Create() where T : notnull + public static IChecksumProvider Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>() where T : notnull { -#if !AOT_ENABLED if (Core.TypeHelpers.HasInvariantHashCode()) return new HashCodeChecksumProvider(); -#endif return new EmptyChecksumProvider(); } diff --git a/tests/Backdash.Tests/Backdash.Tests.csproj b/tests/Backdash.Tests/Backdash.Tests.csproj index c2cb2a16..33aa7fd5 100644 --- a/tests/Backdash.Tests/Backdash.Tests.csproj +++ b/tests/Backdash.Tests/Backdash.Tests.csproj @@ -1,12 +1,13 @@ - net8.0 + net9.0 enable enable false true false CS1591;NU1903 + true diff --git a/tests/Backdash.Tests/Specs/Unit/Serialization/SerializersTests.cs b/tests/Backdash.Tests/Specs/Unit/Serialization/SerializersTests.cs index 26a0c7a8..0a62341c 100644 --- a/tests/Backdash.Tests/Specs/Unit/Serialization/SerializersTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Serialization/SerializersTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using Backdash.Serialization; @@ -225,7 +226,6 @@ static void AssertBaseSerializer(IBinarySerializer serializer) .And .BeOfType>(); -#if !AOT_ENABLED [Fact] public void ShouldReturnCorrectSerializerForStruct() { @@ -233,13 +233,13 @@ public void ShouldReturnCorrectSerializerForStruct() serializer.Should().BeOfType>(); } - static void AssertIntegerSerializer() where T : unmanaged, IBinaryInteger, IMinMaxValue + static void AssertIntegerSerializer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() where T : unmanaged, IBinaryInteger, IMinMaxValue { var serializer = BinarySerializerFactory.Get(); serializer.Should().BeOfType>(); } - static void AssertEnumSerializer() where T : unmanaged, Enum + static void AssertEnumSerializer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() where T : unmanaged, Enum { var serializer = BinarySerializerFactory.Get(); serializer.Should().BeOfType>(); @@ -265,5 +265,4 @@ static void AssertEnumSerializer() where T : unmanaged, Enum [Fact] public void AssertSerializerLongEnum() => AssertEnumSerializer(); [Fact] public void AssertSerializerULongEnum() => AssertEnumSerializer(); -#endif } diff --git a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs index 9345dd72..d752b54e 100644 --- a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPAddressConverterTests.cs @@ -1,6 +1,8 @@ using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; using Backdash.JsonConverters; +using Backdash.Tests.TestUtils; namespace Backdash.Tests.Specs.Unit.Utils; @@ -10,21 +12,14 @@ public class JsonIPAddressConverterTests { static readonly Faker faker = new(); - static readonly JsonSerializerOptions options = new() - { - Converters = - { - new JsonIPAddressConverter(), - }, - }; - record TestType(IPAddress Data); + public record IpAddressTestType([property: JsonConverter(typeof(JsonIPAddressConverter))] IPAddress Data); [Fact] public void ShouldParseIPv4() { var expected = faker.Internet.IpAddress(); - var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", JsonSourceGenerationContext.Default.IpAddressTestType); value!.Data.Should().Be(expected); } @@ -32,7 +27,7 @@ public void ShouldParseIPv4() public void ShouldParseIPv6() { var expected = faker.Internet.Ipv6Address(); - var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected.ToString()}}"}""", JsonSourceGenerationContext.Default.IpAddressTestType); value!.Data.Should().Be(expected); } @@ -40,7 +35,7 @@ public void ShouldParseIPv6() public void ShouldSerializeIPv4() { var value = faker.Internet.IpAddress(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpAddressTestType(value), JsonSourceGenerationContext.Default.IpAddressTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } @@ -49,7 +44,7 @@ public void ShouldSerializeIPv4() public void ShouldSerializeIPv6() { var value = faker.Internet.Ipv6Address(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpAddressTestType(value), JsonSourceGenerationContext.Default.IpAddressTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } diff --git a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs index 54f5dd4f..6b006600 100644 --- a/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Utils/JsonIPEndpointConverterTests.cs @@ -1,6 +1,8 @@ using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; using Backdash.JsonConverters; +using Backdash.Tests.TestUtils; namespace Backdash.Tests.Specs.Unit.Utils; @@ -10,21 +12,13 @@ public class JsonIPEndpointConverterTests { static readonly Faker faker = new(); - static readonly JsonSerializerOptions options = new() - { - Converters = - { - new JsonIPEndPointConverter(), - }, - }; - - record TestType(IPEndPoint Data); + public record IpEndPointTestType([property: JsonConverter(typeof(JsonIPEndPointConverter))] IPEndPoint Data); [Fact] public void ShouldParseIPv4() { var expected = faker.Internet.IpEndPoint(); - var value = Deserialize($$"""{"Data": "{{expected}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected}}"}""", JsonSourceGenerationContext.Default.IpEndPointTestType); value!.Data.Should().Be(expected); } @@ -32,7 +26,7 @@ public void ShouldParseIPv4() public void ShouldParseIPv6() { var expected = faker.Internet.Ipv6EndPoint(); - var value = Deserialize($$"""{"Data": "{{expected}}"}""", options); + var value = Deserialize($$"""{"Data": "{{expected}}"}""", JsonSourceGenerationContext.Default.IpEndPointTestType); value!.Data.Should().Be(expected); } @@ -40,7 +34,7 @@ public void ShouldParseIPv6() public void ShouldSerializeIPv4() { var value = faker.Internet.IpEndPoint(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpEndPointTestType(value), JsonSourceGenerationContext.Default.IpEndPointTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } @@ -49,7 +43,7 @@ public void ShouldSerializeIPv4() public void ShouldSerializeIPv6() { var value = faker.Internet.Ipv6EndPoint(); - var result = Serialize(new TestType(value), options); + var result = Serialize(new IpEndPointTestType(value), JsonSourceGenerationContext.Default.IpEndPointTestType); var expected = $$"""{"Data":"{{value}}"}"""; result.Should().Be(expected); } diff --git a/tests/Backdash.Tests/TestUtils/JsonSourceGenerationContext.cs b/tests/Backdash.Tests/TestUtils/JsonSourceGenerationContext.cs new file mode 100644 index 00000000..b5c0395e --- /dev/null +++ b/tests/Backdash.Tests/TestUtils/JsonSourceGenerationContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using Backdash.Tests.Specs.Unit.Utils; + +namespace Backdash.Tests.TestUtils +{ + + [JsonSerializable(typeof(JsonIPAddressConverterTests.IpAddressTestType))] + [JsonSerializable(typeof(JsonIPEndpointConverterTests.IpEndPointTestType))] + partial class JsonSourceGenerationContext : JsonSerializerContext + { + } +} diff --git a/tests/Backdash.Tests/TestUtils/TestInput.cs b/tests/Backdash.Tests/TestUtils/TestInput.cs index 1bd8d904..9f3f060f 100644 --- a/tests/Backdash.Tests/TestUtils/TestInput.cs +++ b/tests/Backdash.Tests/TestUtils/TestInput.cs @@ -12,6 +12,19 @@ public struct TestInputBuffer public readonly string ToString(bool trimZeros) => Mem.GetBitString(this, trimRightZeros: trimZeros); public override readonly string ToString() => ToString(trimZeros: true); + + /// + public override readonly int GetHashCode() => Mem.GetHashCode(this); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public readonly bool Equals(TestInputBuffer other) => this[..].SequenceEqual(other); + + /// + public override readonly bool Equals(object? obj) => obj is TestInputBuffer other && Equals(other); } [Serializable] public record struct TestInput