Refactor BenchmarkTestDataProvider for flexibility & clarity
Moved BenchmarkTestDataProvider and TestDataSet to AyCode.Core.Tests.TestModels with public accessibility. Refactored dataset creation methods to accept a resetId parameter, allowing control over TestDataFactory ID resets. Improved code structure, formatting, and documentation for maintainability. The provider is now more flexible and easier to use in tests.
This commit is contained in:
parent
accb38cf75
commit
26c8cd85ce
|
|
@ -1,23 +1,22 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console;
|
||||
namespace AyCode.Core.Tests.TestModels;
|
||||
|
||||
internal static class BenchmarkTestDataProvider
|
||||
public static class BenchmarkTestDataProvider
|
||||
{
|
||||
internal static List<TestDataSet> CreateTestDataSets()
|
||||
public static List<TestDataSet> CreateTestDataSets(bool resetId = true)
|
||||
{
|
||||
return new List<TestDataSet>
|
||||
{
|
||||
CreateSmallTestData(),
|
||||
CreateMediumTestData(),
|
||||
CreateLargeTestData(),
|
||||
CreateRepeatedStringsTestData(),
|
||||
CreateDeepNestedTestData()
|
||||
CreateSmallTestData(resetId),
|
||||
CreateMediumTestData(resetId),
|
||||
CreateLargeTestData(resetId),
|
||||
CreateRepeatedStringsTestData(resetId),
|
||||
CreateDeepNestedTestData(resetId)
|
||||
};
|
||||
}
|
||||
|
||||
internal static TestOrder CreateProfilerOrder()
|
||||
public static TestOrder CreateProfilerOrder()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
|
|
@ -31,9 +30,9 @@ internal static class BenchmarkTestDataProvider
|
|||
sharedUser: sharedUser);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateSmallTestData()
|
||||
private static TestDataSet CreateSmallTestData(bool resetId = true)
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
if (resetId) TestDataFactory.ResetIdCounter();
|
||||
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
|
|
@ -51,9 +50,9 @@ internal static class BenchmarkTestDataProvider
|
|||
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateMediumTestData()
|
||||
private static TestDataSet CreateMediumTestData(bool resetId = true)
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
if (resetId) TestDataFactory.ResetIdCounter();
|
||||
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
|
|
@ -83,9 +82,9 @@ internal static class BenchmarkTestDataProvider
|
|||
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateLargeTestData()
|
||||
private static TestDataSet CreateLargeTestData(bool resetId = true)
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
if (resetId) TestDataFactory.ResetIdCounter();
|
||||
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
|
|
@ -113,9 +112,9 @@ internal static class BenchmarkTestDataProvider
|
|||
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateRepeatedStringsTestData()
|
||||
private static TestDataSet CreateRepeatedStringsTestData(bool resetId = true)
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
if (resetId) TestDataFactory.ResetIdCounter();
|
||||
|
||||
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("repeateduser");
|
||||
|
|
@ -149,9 +148,9 @@ internal static class BenchmarkTestDataProvider
|
|||
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateDeepNestedTestData()
|
||||
private static TestDataSet CreateDeepNestedTestData(bool resetId = true)
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
if (resetId) TestDataFactory.ResetIdCounter();
|
||||
|
||||
var sharedTag = TestDataFactory.CreateTag("DeepTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("deepuser");
|
||||
|
|
@ -207,7 +206,7 @@ internal static class BenchmarkTestDataProvider
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class TestDataSet
|
||||
public sealed class TestDataSet
|
||||
{
|
||||
public string Name { get; }
|
||||
public TestOrder Order { get; }
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
|
@ -140,10 +141,21 @@ public static partial class AcBinaryDeserializer
|
|||
var flags = ints[3];
|
||||
var isNegative = (flags & unchecked((int)0x80000000)) != 0;
|
||||
var scale = (byte)((flags >> 16) & 0x7F);
|
||||
LogDecimalDrift(scale);
|
||||
_position += 16;
|
||||
return new decimal(lo, mid, hi, isNegative, scale);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void LogDecimalDrift(byte scale)
|
||||
{
|
||||
if (scale <= 28) return;
|
||||
var hex = BitConverter.ToString(_buffer, _position, Math.Min(16, _bufferLength - _position));
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"[DECIMAL_DRIFT] scale={scale}, pos={_position}, bufLen={_bufferLength}, " +
|
||||
$"bufArray={_buffer.Length}, hex={hex}", _position);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime ReadDateTimeUnsafe()
|
||||
{
|
||||
|
|
@ -444,7 +456,17 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, length))
|
||||
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
|
||||
AssertGuarantee(length);
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void AssertGuarantee(int needed)
|
||||
{
|
||||
if (_bufferLength - _position < needed)
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"[GUARANTEE_VIOLATED] TryAdvanceSegment returned true but available={_bufferLength - _position} < needed={needed}, " +
|
||||
$"pos={_position}, bufLen={_bufferLength}, bufArray={_buffer.Length}", _position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,13 +82,18 @@ public struct SequenceBinaryInput : IBinaryInputBase
|
|||
var remaining = bufferLength - position;
|
||||
|
||||
if (remaining > 0 && remaining < needed)
|
||||
{
|
||||
// Cross-boundary: value spans segment boundary
|
||||
return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining);
|
||||
}
|
||||
|
||||
// Current segment fully consumed — advance to next
|
||||
return TryLoadNextSegment(ref buffer, ref position, ref bufferLength);
|
||||
if (!TryLoadNextSegment(ref buffer, ref position, ref bufferLength))
|
||||
return false;
|
||||
|
||||
// Loaded segment smaller than needed — cross-boundary into subsequent segments
|
||||
remaining = bufferLength - position;
|
||||
if (remaining < needed)
|
||||
return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1046,6 +1046,30 @@ public abstract class SignalRClientToHubTestBase
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Dataset Tests
|
||||
|
||||
[TestMethod]
|
||||
public async Task RoundTrip_LargeOrderList_PreservesAllData()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var dataSets = BenchmarkTestDataProvider.CreateTestDataSets(resetId: false);
|
||||
var orders = dataSets.Select(ds => ds.Order).ToList();
|
||||
|
||||
var result = await _client.PostDataAsync<List<TestOrder>, List<TestOrder>>(
|
||||
TestSignalRTags.TestOrderListParam, orders);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(orders.Count, result.Count);
|
||||
for (int i = 0; i < orders.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(orders[i].Id, result[i].Id);
|
||||
Assert.AreEqual(orders[i].OrderNumber, result[i].OrderNumber);
|
||||
Assert.AreEqual(orders[i].Items.Count, result[i].Items.Count);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Buffers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Pipelines;
|
||||
using AyCode.Services.SignalRs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
|
|
@ -7,32 +8,60 @@ using Microsoft.AspNetCore.SignalR.Protocol;
|
|||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Test protocol that forces multi-segment ReadOnlySequence parsing.
|
||||
/// Splits serialized bytes into chunks before calling base.TryParseMessage,
|
||||
/// exercising SequenceBinaryInput cross-boundary reads and SequenceToByteArray multi-segment paths.
|
||||
/// Test protocol that simulates production Kestrel pipe behavior.
|
||||
///
|
||||
/// Write side: uses Pipe (not ArrayBufferWriter) so GetSpan/GetMemory return stable slab segments
|
||||
/// — matching Kestrel's memory pool behavior. This ensures Span back-patching for length prefixes works.
|
||||
///
|
||||
/// Read side: splits the serialized bytes into 256-byte segments before parsing,
|
||||
/// exercising SequenceBinaryInput cross-boundary reads at every boundary.
|
||||
/// </summary>
|
||||
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
|
||||
{
|
||||
private const int SegmentSize = 4096;
|
||||
private const int SegmentSize = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Serialize via Pipe (production-like stable memory blocks) instead of ArrayBufferWriter.
|
||||
/// </summary>
|
||||
public new ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
WriteMessage(message, pipe.Writer);
|
||||
pipe.Writer.Complete();
|
||||
pipe.Reader.TryRead(out var result);
|
||||
var bytes = result.Buffer.ToArray();
|
||||
pipe.Reader.Complete();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split input into 256-byte segments before parsing — forces multi-segment ReadOnlySequence
|
||||
/// through SequenceBinaryInput, exercising cross-boundary reads on every test.
|
||||
/// </summary>
|
||||
public override bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder,
|
||||
[NotNullWhen(true)] out HubMessage? message)
|
||||
{
|
||||
// Temporarily bypass multi-segment to isolate the issue
|
||||
return base.TryParseMessage(ref input, binder, out message);
|
||||
var multiSegment = CreateMultiSegmentSequence(input, SegmentSize);
|
||||
return base.TryParseMessage(ref multiSegment, binder, out message);
|
||||
}
|
||||
|
||||
private static ReadOnlySequence<byte> CreateMultiSegmentSequence(ReadOnlySequence<byte> source, int chunkSize)
|
||||
{
|
||||
var bytes = source.ToArray();
|
||||
|
||||
var first = new MemorySegment(bytes.AsMemory(0, Math.Min(chunkSize, bytes.Length)));
|
||||
// Each segment gets its own byte[] — matching Kestrel pool slab behavior
|
||||
// where each pipe segment is a separate memory block.
|
||||
var firstChunk = new byte[Math.Min(chunkSize, bytes.Length)];
|
||||
Buffer.BlockCopy(bytes, 0, firstChunk, 0, firstChunk.Length);
|
||||
var first = new MemorySegment(firstChunk);
|
||||
var current = first;
|
||||
|
||||
for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
|
||||
{
|
||||
var length = Math.Min(chunkSize, bytes.Length - offset);
|
||||
current = current.Append(bytes.AsMemory(offset, length));
|
||||
var chunk = new byte[length];
|
||||
Buffer.BlockCopy(bytes, offset, chunk, 0, length);
|
||||
current = current.Append(chunk);
|
||||
}
|
||||
|
||||
return new ReadOnlySequence<byte>(first, 0, current, current.Memory.Length);
|
||||
|
|
|
|||
|
|
@ -261,6 +261,16 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
[Conditional("DEBUG")]
|
||||
private static void LogDiagnostic(string message) => DiagnosticLogger?.Invoke(message);
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void LogReadSingleArgument(ReadOnlySequence<byte> argSlice, int argLength, Type targetType)
|
||||
{
|
||||
if (DiagnosticLogger == null) return;
|
||||
var segmentCount = 0;
|
||||
foreach (var _ in argSlice)
|
||||
segmentCount++;
|
||||
DiagnosticLogger($"[AcBinaryHubProtocol] ReadSingleArgument: argLength={argLength}, isSingleSegment={argSlice.IsSingleSegment}, segments={segmentCount}, type={targetType.Name}");
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void LogParseInvocation(string target, IReadOnlyList<Type> paramTypes, long remaining)
|
||||
{
|
||||
|
|
@ -455,6 +465,8 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
var argSlice = r.UnreadSequence.Slice(0, argLength);
|
||||
r.Advance(argLength);
|
||||
|
||||
LogReadSingleArgument(argSlice, argLength, targetType);
|
||||
|
||||
// byte[] fast-path: first byte is BinaryTypeCode.ByteArray tag →
|
||||
// strip tag + VarUInt length prefix, return raw payload. No deserializer.
|
||||
var argReader = new SequenceReader<byte>(argSlice);
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ namespace AyCode.Services.SignalRs
|
|||
if (useAcBinaryProtocol)
|
||||
{
|
||||
hubBuilder.Services.AddSingleton<IHubProtocol, AyCodeBinaryHubProtocol>();
|
||||
AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg);
|
||||
}
|
||||
|
||||
HubConnection = hubBuilder.Build();
|
||||
|
|
|
|||
Loading…
Reference in New Issue