using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using AyCode.Services.SignalRs; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Protocol; namespace AyCode.Services.Server.Tests.SignalRs; /// /// Test protocol that simulates production Kestrel pipe behavior with 256-byte segments. /// /// Production: SignalR → WriteMessage(PipeWriter) → Kestrel slab 4096-byte segments → PipeReader → TryParseMessage /// Test: SignalR → WriteMessage(PipeWriter) → FixedSizePool 256-byte segments → PipeReader → TryParseMessage /// /// Both sides go through a real Pipe with fixed-size memory segments, /// exercising BWO chunk writes and SequenceBinaryInput cross-boundary reads. /// internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol { private const int SegmentSize = 256; public TestMultiSegmentProtocol() { Options.BufferWriterChunkSize = SegmentSize; } /// /// Write side: WriteMessage → PipeWriter backed by 256-byte pool segments. /// Same code path as production, just smaller segments. /// public new ReadOnlyMemory GetMessageBytes(HubMessage message) { var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize))); WriteMessage(message, pipe.Writer); pipe.Writer.Complete(); pipe.Reader.TryRead(out var result); var bytes = result.Buffer.ToArray(); pipe.Reader.Complete(); return bytes; } /// /// Read side: fill PipeWriter 256 bytes at a time → PipeReader gives multi-segment sequence. /// Same as production Kestrel PipeReader delivering 4096-byte segments. /// public override bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, [NotNullWhen(true)] out HubMessage? message) { var bytes = input.ToArray(); var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize))); var writer = pipe.Writer; // Write in chunks — GetMemory may return less than requested (like Kestrel slab) var remaining = bytes.AsSpan(); while (remaining.Length > 0) { var mem = writer.GetMemory(Math.Min(SegmentSize, remaining.Length)); var chunk = Math.Min(mem.Length, remaining.Length); remaining[..chunk].CopyTo(mem.Span); writer.Advance(chunk); remaining = remaining[chunk..]; } writer.Complete(); pipe.Reader.TryRead(out var result); var seq = result.Buffer; var success = base.TryParseMessage(ref seq, binder, out message); pipe.Reader.Complete(); return success; } /// /// MemoryPool that returns -byte blocks at random offsets /// within a larger backing array — simulating Kestrel's slab allocator where segments /// share a large slab and have non-zero offsets. /// private sealed class SlabSimulatingPool(int segmentSize) : MemoryPool { private readonly Random _rng = new(42); // deterministic seed for reproducibility public override int MaxBufferSize => segmentSize; public override IMemoryOwner Rent(int minBufferSize = -1) { var size = Math.Max(minBufferSize, segmentSize); var offset = _rng.Next(0, segmentSize); // random slab offset var jitter = _rng.Next(-1, 2); // -1, 0, or +1 var actualSize = Math.Max(1, size + jitter); // random segment size variance var array = new byte[actualSize + offset]; return new Owner(array, offset, actualSize); } protected override void Dispose(bool disposing) { } private sealed class Owner(byte[] array, int offset, int length) : IMemoryOwner { public Memory Memory { get; } = array.AsMemory(offset, length); public void Dispose() { } } } }