AyCode.Core/AyCode.Core.Tests/Serialization/AcBinarySerializerNamedPipe...

179 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
using static AyCode.Core.Tests.TestModels.AcSerializerModels;
namespace AyCode.Core.Tests.Serialization;
/// <summary>
/// Cross-platform NamedPipe IPC roundtrip tests for AcBinarySerializer's full-lifecycle helpers
/// (Step 4 of ADR-0003, ACCORE-BIN-T-A3T8).
///
/// <para><c>SerializeToNamedPipeAsync</c> and <c>DeserializeFromNamedPipeAsync</c> internally
/// exercise the full streaming pipeline: <c>AcBinarySerializer.Serialize → PipeWriter →
/// NamedPipe → PipeReader → AsyncPipeReaderInput.DrainFromAsync → AcBinaryDeserializer.Deserialize</c>.
/// With <c>BufferWriterChunkSize = 256</c>, even small test payloads cross multiple chunk
/// boundaries on the wire — exercises the real chunking + sliding-window cycling behavior
/// instead of the "fits-in-one-chunk" degenerate case.</para>
/// </summary>
[TestClass]
public class AcBinarySerializerNamedPipeTests
{
[TestMethod]
public async Task RoundTrip_SmallChunkSize_PayloadEquals()
{
// Unique pipe name per test run to avoid cross-run interference.
var pipeName = $"AcBinaryTest-{Guid.NewGuid():N}";
// 256-byte chunk size = Kestrel slab default; the AsyncPipeWriterOutput on a
// StreamPipeWriter (NamedPipe-backed) currently misbehaves on chunkSize < 256
// (ArgumentOutOfRangeException in StreamPipeWriter.Advance — pre-existing latent
// issue in AsyncPipeWriterOutput, not introduced here). Tracked separately; this
// test uses a known-working chunk size that still exercises framing across
// multiple chunks for our 50-item payload.
var opts = new AcBinarySerializerOptions { BufferWriterChunkSize = 256 };
var original = CreatePayload(50);
// Start the receiver first — DeserializeFromNamedPipeAsync's synchronous prefix
// (NamedPipeServerStream ctor) runs before the first await, so the pipe is bound
// by the time this line returns and the client can immediately connect.
var receiveTask = AcBinaryDeserializer.DeserializeFromNamedPipeAsync<TestParentWithDateTimeItemCollection>(pipeName, opts);
await AcBinarySerializer.SerializeToNamedPipeAsync(pipeName, original, opts);
var result = await receiveTask;
Assert.IsNotNull(result);
AssertPayloadEquals(original, result);
}
[TestMethod]
public async Task RoundTrip_LargeScalePayload_ChunkSize256_StructuralEquality()
{
// Production-scale payload via TestDataFactory: 100 root items × 3 pallets × 3 measurements × 4 points
// = ~3700 deeply-nested objects with shared references (50 tags, 20 users, metadata, 10 categories).
// Serialized size ~few hundred KB → many chunks at chunkSize=256 → real backpressure-driven streaming
// (PipeWriter pauseThreshold ~64KB, bytes flow incrementally as consumer drains).
#if DEBUG
// Capture BOTH receiver and sender state to diagnose the StreamPipeWriter interaction.
var diagLogs = new List<string>();
AsyncPipeReaderInput.DiagnosticLog = msg => diagLogs.Add($"[R] {msg}");
AsyncPipeWriterOutput.DiagnosticLog = msg => diagLogs.Add($"[S] {msg}");
#endif
try
{
var pipeName = $"AcBinaryTest-{Guid.NewGuid():N}";
var opts = new AcBinarySerializerOptions { BufferWriterChunkSize = 256 };
var original = TestDataFactory.CreateLargeScaleBenchmarkOrder(rootItemCount: 100);
var receiveTask = AcBinaryDeserializer.DeserializeFromNamedPipeAsync<TestOrder>(pipeName, opts);
await AcBinarySerializer.SerializeToNamedPipeAsync(pipeName, original, opts);
var result = await receiveTask;
Assert.IsNotNull(result);
Assert.AreEqual(original.Id, result.Id);
Assert.AreEqual(original.OrderNumber, result.OrderNumber);
Assert.AreEqual(original.Status, result.Status);
Assert.AreEqual(original.TotalAmount, result.TotalAmount);
// Deep structure: count items + pallets + measurements + points must match exactly
var origCounts = CountTestOrderHierarchy(original);
var resultCounts = CountTestOrderHierarchy(result);
Assert.AreEqual(origCounts.items, resultCounts.items, "Items count mismatch");
Assert.AreEqual(origCounts.pallets, resultCounts.pallets, "Pallets count mismatch");
Assert.AreEqual(origCounts.measurements, resultCounts.measurements, "Measurements count mismatch");
Assert.AreEqual(origCounts.points, resultCounts.points, "Points count mismatch");
}
finally
{
#if DEBUG
AsyncPipeReaderInput.DiagnosticLog = null;
AsyncPipeWriterOutput.DiagnosticLog = null;
if (diagLogs.Count > 0)
{
Console.WriteLine($"=== Sender [S] + Receiver [R] DiagnosticLog trail ({diagLogs.Count} entries) ===");
// Print last 60 entries (most relevant to failure point)
var startIdx = Math.Max(0, diagLogs.Count - 60);
if (startIdx > 0)
Console.WriteLine($" ... ({startIdx} earlier entries elided)");
for (var i = startIdx; i < diagLogs.Count; i++)
Console.WriteLine($" [{i}] {diagLogs[i]}");
Console.WriteLine($"=== End DiagnosticLog ===");
}
#endif
}
}
private static (int items, int pallets, int measurements, int points) CountTestOrderHierarchy(TestOrder order)
{
int items = order.Items.Count;
int pallets = 0, measurements = 0, points = 0;
foreach (var item in order.Items)
{
pallets += item.Pallets.Count;
foreach (var p in item.Pallets)
{
measurements += p.Measurements.Count;
foreach (var m in p.Measurements)
points += m.Points.Count;
}
}
return (items, pallets, measurements, points);
}
// Note: a "default chunk size" test was deliberately omitted. The default
// AcBinarySerializerOptions.BufferWriterChunkSize used to be 65536, which exceeded the
// UINT16 max (65535). Fixed in this work to 65535. Tests above explicitly set chunk size
// for reproducibility regardless of default.
private static TestParentWithDateTimeItemCollection CreatePayload(int itemCount)
{
var now = DateTime.UtcNow;
var items = new List<TestEntityWithDateTimeAndInt>(itemCount);
for (var i = 0; i < itemCount; i++)
{
items.Add(new TestEntityWithDateTimeAndInt
{
Id = i + 1,
IntValue = i * 3,
Created = now.AddMinutes(-i),
Modified = now.AddMinutes(i),
StatusCode = i % 4,
Name = $"item-{i}"
});
}
return new TestParentWithDateTimeItemCollection
{
Id = 11,
Name = "named-pipe-roundtrip",
Created = now,
Items = items
};
}
private static void AssertPayloadEquals(TestParentWithDateTimeItemCollection expected, TestParentWithDateTimeItemCollection actual)
{
Assert.AreEqual(expected.Id, actual.Id);
Assert.AreEqual(expected.Name, actual.Name);
Assert.AreEqual(expected.Created, actual.Created);
Assert.IsNotNull(expected.Items);
Assert.IsNotNull(actual.Items);
Assert.AreEqual(expected.Items.Count, actual.Items.Count);
for (var i = 0; i < expected.Items.Count; i++)
{
var e = expected.Items[i];
var a = actual.Items[i];
Assert.AreEqual(e.Id, a.Id);
Assert.AreEqual(e.IntValue, a.IntValue);
Assert.AreEqual(e.Created, a.Created);
Assert.AreEqual(e.Modified, a.Modified);
Assert.AreEqual(e.StatusCode, a.StatusCode);
Assert.AreEqual(e.Name, a.Name);
}
}
}