perf(net): borrow inbound packet storage
Decode headers, optional fields, fragments, and single-fragment messages directly over pooled datagrams. Copy only fragment state that crosses a datagram lifetime, preserve synchronous dispatch and ACK ordering, and lock the path to the owned decoder with differential and zero-allocation tests.
This commit is contained in:
parent
7211bb1bf7
commit
e928c5dd02
18 changed files with 1329 additions and 66 deletions
441
tests/AcDream.Core.Net.Tests/Packets/BorrowedPacketCodecTests.cs
Normal file
441
tests/AcDream.Core.Net.Tests/Packets/BorrowedPacketCodecTests.cs
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Cryptography;
|
||||
using AcDream.Core.Net.Packets;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Packets;
|
||||
|
||||
public class BorrowedPacketCodecTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryDecodeBorrowed_AllOptionalFieldsAndFragments_MatchesOwnedDecoder()
|
||||
{
|
||||
const PacketHeaderFlags flags =
|
||||
PacketHeaderFlags.ServerSwitch
|
||||
| PacketHeaderFlags.RequestRetransmit
|
||||
| PacketHeaderFlags.RejectRetransmit
|
||||
| PacketHeaderFlags.AckSequence
|
||||
| PacketHeaderFlags.WorldLoginRequest
|
||||
| PacketHeaderFlags.ConnectRequest
|
||||
| PacketHeaderFlags.ConnectResponse
|
||||
| PacketHeaderFlags.CICMDCommand
|
||||
| PacketHeaderFlags.TimeSync
|
||||
| PacketHeaderFlags.EchoRequest
|
||||
| PacketHeaderFlags.Flow
|
||||
| PacketHeaderFlags.BlobFragments;
|
||||
|
||||
var optional = new byte[8 + 12 + 8 + 4 + 8 + 32 + 8 + 8 + 8 + 4 + 6];
|
||||
int position = 0;
|
||||
Fill(optional, ref position, 8, 0x11);
|
||||
|
||||
WriteUInt32(optional, ref position, 2);
|
||||
WriteUInt32(optional, ref position, 0x10111213);
|
||||
WriteUInt32(optional, ref position, 0x20212223);
|
||||
|
||||
WriteUInt32(optional, ref position, 1);
|
||||
WriteUInt32(optional, ref position, 0x30313233);
|
||||
|
||||
WriteUInt32(optional, ref position, 0x40414243);
|
||||
Fill(optional, ref position, 8, 0x44);
|
||||
|
||||
double serverTime = 12345.6789;
|
||||
ulong cookie = 0xFEEDFACECAFEBABE;
|
||||
uint clientId = 0x50515253;
|
||||
uint serverSeed = 0x60616263;
|
||||
uint clientSeed = 0x70717273;
|
||||
WriteDouble(optional, ref position, serverTime);
|
||||
WriteUInt64(optional, ref position, cookie);
|
||||
WriteUInt32(optional, ref position, clientId);
|
||||
WriteUInt32(optional, ref position, serverSeed);
|
||||
WriteUInt32(optional, ref position, clientSeed);
|
||||
WriteUInt32(optional, ref position, 0);
|
||||
|
||||
Fill(optional, ref position, 8, 0x81);
|
||||
Fill(optional, ref position, 8, 0x91);
|
||||
|
||||
double timeSync = 9876.54321;
|
||||
float echoTime = 42.25f;
|
||||
WriteDouble(optional, ref position, timeSync);
|
||||
WriteSingle(optional, ref position, echoTime);
|
||||
WriteUInt32(optional, ref position, 0xA0A1A2A3);
|
||||
WriteUInt16(optional, ref position, 0xB0B1);
|
||||
Assert.Equal(optional.Length, position);
|
||||
|
||||
byte[] first = BuildFragment(
|
||||
sequence: 99,
|
||||
count: 2,
|
||||
index: 0,
|
||||
queue: 7,
|
||||
payload: [0xC1, 0xC2]);
|
||||
byte[] second = BuildFragment(
|
||||
sequence: 99,
|
||||
count: 2,
|
||||
index: 1,
|
||||
queue: 7,
|
||||
payload: [0xD1, 0xD2, 0xD3]);
|
||||
|
||||
byte[] body = [.. optional, .. first, .. second];
|
||||
byte[] datagram = Encode(flags, body);
|
||||
|
||||
PacketCodec.PacketDecodeResult owned =
|
||||
PacketCodec.TryDecode(datagram, inboundIsaac: null);
|
||||
BorrowedPacketDecodeResult borrowed =
|
||||
PacketCodec.TryDecodeBorrowed(datagram, inboundIsaac: null);
|
||||
|
||||
AssertEquivalent(owned, borrowed);
|
||||
Assert.Equal(serverTime, borrowed.Packet.Optional.ConnectRequestServerTime);
|
||||
Assert.Equal(cookie, borrowed.Packet.Optional.ConnectRequestCookie);
|
||||
Assert.Equal(clientId, borrowed.Packet.Optional.ConnectRequestClientId);
|
||||
Assert.Equal(serverSeed, borrowed.Packet.Optional.ConnectRequestServerSeed);
|
||||
Assert.Equal(clientSeed, borrowed.Packet.Optional.ConnectRequestClientSeed);
|
||||
Assert.Equal(timeSync, borrowed.Packet.Optional.TimeSync);
|
||||
Assert.Equal(echoTime, borrowed.Packet.Optional.EchoRequestClientTime);
|
||||
Assert.Equal(0xA0A1A2A3u, borrowed.Packet.Optional.FlowBytes);
|
||||
Assert.Equal(0xB0B1, borrowed.Packet.Optional.FlowInterval);
|
||||
Assert.Equal(
|
||||
new uint[] { 0x10111213, 0x20212223 },
|
||||
ReadRetransmits(borrowed.Packet.Optional));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDecodeBorrowed_LoginPayload_MatchesOwnedDecoder()
|
||||
{
|
||||
byte[] payload = LoginRequest.Build("borrowed", "packet", 47);
|
||||
byte[] datagram = Encode(PacketHeaderFlags.LoginRequest, payload);
|
||||
|
||||
PacketCodec.PacketDecodeResult owned =
|
||||
PacketCodec.TryDecode(datagram, inboundIsaac: null);
|
||||
BorrowedPacketDecodeResult borrowed =
|
||||
PacketCodec.TryDecodeBorrowed(datagram, inboundIsaac: null);
|
||||
|
||||
AssertEquivalent(owned, borrowed);
|
||||
Assert.Equal(payload, borrowed.Packet.Optional.RawBytes.ToArray());
|
||||
Assert.Equal(payload, borrowed.Packet.Body.ToArray());
|
||||
Assert.Equal(0, borrowed.Packet.FragmentCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDecodeBorrowed_EncryptedChecksum_MatchesOwnedDecoder()
|
||||
{
|
||||
byte[] body = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, 0x12345678);
|
||||
byte[] seed = [0x44, 0x33, 0x22, 0x11];
|
||||
byte[] datagram = Encode(
|
||||
PacketHeaderFlags.AckSequence
|
||||
| PacketHeaderFlags.EncryptedChecksum,
|
||||
body,
|
||||
new IsaacRandom(seed));
|
||||
|
||||
PacketCodec.PacketDecodeResult owned =
|
||||
PacketCodec.TryDecode(
|
||||
datagram,
|
||||
new IsaacRandom(seed));
|
||||
BorrowedPacketDecodeResult borrowed =
|
||||
PacketCodec.TryDecodeBorrowed(
|
||||
datagram,
|
||||
new IsaacRandom(seed));
|
||||
|
||||
AssertEquivalent(owned, borrowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDecodeBorrowed_MalformedPackets_MatchOwnedDecoderErrors()
|
||||
{
|
||||
byte[] shortHeader = new byte[PacketHeader.Size - 1];
|
||||
AssertSameError(shortHeader);
|
||||
|
||||
var oversized = new byte[PacketHeader.Size + 2];
|
||||
new PacketHeader { DataSize = 3 }.Pack(oversized);
|
||||
AssertSameError(oversized);
|
||||
|
||||
byte[] shortOptional = EncodeUnchecked(
|
||||
PacketHeaderFlags.TimeSync,
|
||||
[0x01, 0x02, 0x03, 0x04]);
|
||||
AssertSameError(shortOptional);
|
||||
|
||||
byte[] zeroCount = BuildFragment(
|
||||
sequence: 1,
|
||||
count: 0,
|
||||
index: 0,
|
||||
queue: 0,
|
||||
payload: [0x01]);
|
||||
AssertSameError(EncodeUnchecked(
|
||||
PacketHeaderFlags.BlobFragments,
|
||||
zeroCount));
|
||||
|
||||
byte[] invalidIndex = BuildFragment(
|
||||
sequence: 1,
|
||||
count: 1,
|
||||
index: 1,
|
||||
queue: 0,
|
||||
payload: [0x01]);
|
||||
AssertSameError(EncodeUnchecked(
|
||||
PacketHeaderFlags.BlobFragments,
|
||||
invalidIndex));
|
||||
|
||||
byte[] wrongChecksum = Encode(
|
||||
PacketHeaderFlags.AckSequence,
|
||||
[1, 2, 3, 4]);
|
||||
wrongChecksum[8] ^= 0x80;
|
||||
AssertSameError(wrongChecksum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDecodeBorrowed_WarmSingleFragmentPath_AllocatesNothing()
|
||||
{
|
||||
byte[] fragment = BuildFragment(
|
||||
sequence: 77,
|
||||
count: 1,
|
||||
index: 0,
|
||||
queue: 5,
|
||||
payload: [0xAA, 0xBB, 0xCC, 0xDD]);
|
||||
byte[] datagram = Encode(
|
||||
PacketHeaderFlags.BlobFragments,
|
||||
fragment);
|
||||
|
||||
int checksum = DecodeAndRead(datagram);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int iteration = 0; iteration < 1_000; iteration++)
|
||||
checksum += DecodeAndRead(datagram);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.NotEqual(0, checksum);
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
private static int DecodeAndRead(ReadOnlyMemory<byte> datagram)
|
||||
{
|
||||
BorrowedPacketDecodeResult decoded =
|
||||
PacketCodec.TryDecodeBorrowed(
|
||||
datagram,
|
||||
inboundIsaac: null);
|
||||
int checksum = decoded.Packet.FragmentCount;
|
||||
foreach (BorrowedMessageFragment fragment
|
||||
in decoded.Packet.Fragments)
|
||||
{
|
||||
checksum += fragment.Header.TotalSize;
|
||||
checksum += fragment.Payload.Span[0];
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
private static void AssertEquivalent(
|
||||
PacketCodec.PacketDecodeResult owned,
|
||||
BorrowedPacketDecodeResult borrowed)
|
||||
{
|
||||
Assert.Equal(owned.Error, borrowed.Error);
|
||||
if (!owned.IsOk)
|
||||
return;
|
||||
|
||||
Packet packet = Assert.IsType<Packet>(owned.Packet);
|
||||
Assert.Equal(packet.Header, borrowed.Packet.Header);
|
||||
Assert.Equal(packet.BodyBytes, borrowed.Packet.Body.ToArray());
|
||||
Assert.Equal(
|
||||
packet.Optional.RawBytes,
|
||||
borrowed.Packet.Optional.RawBytes.ToArray());
|
||||
Assert.Equal(
|
||||
packet.Optional.AckSequence,
|
||||
borrowed.Packet.Optional.AckSequence);
|
||||
Assert.Equal(
|
||||
packet.Optional.TimeSync,
|
||||
borrowed.Packet.Optional.TimeSync);
|
||||
Assert.Equal(
|
||||
packet.Optional.EchoRequestClientTime,
|
||||
borrowed.Packet.Optional.EchoRequestClientTime);
|
||||
Assert.Equal(
|
||||
packet.Optional.FlowBytes,
|
||||
borrowed.Packet.Optional.FlowBytes);
|
||||
Assert.Equal(
|
||||
packet.Optional.FlowInterval,
|
||||
borrowed.Packet.Optional.FlowInterval);
|
||||
Assert.Equal(
|
||||
packet.Optional.ConnectRequestServerTime,
|
||||
borrowed.Packet.Optional.ConnectRequestServerTime);
|
||||
Assert.Equal(
|
||||
packet.Optional.ConnectRequestCookie,
|
||||
borrowed.Packet.Optional.ConnectRequestCookie);
|
||||
Assert.Equal(
|
||||
packet.Optional.ConnectRequestClientId,
|
||||
borrowed.Packet.Optional.ConnectRequestClientId);
|
||||
Assert.Equal(
|
||||
packet.Optional.ConnectRequestServerSeed,
|
||||
borrowed.Packet.Optional.ConnectRequestServerSeed);
|
||||
Assert.Equal(
|
||||
packet.Optional.ConnectRequestClientSeed,
|
||||
borrowed.Packet.Optional.ConnectRequestClientSeed);
|
||||
Assert.Equal(
|
||||
packet.Optional.RetransmitRequests,
|
||||
ReadRetransmits(borrowed.Packet.Optional));
|
||||
|
||||
var fragments = new List<BorrowedMessageFragment>();
|
||||
foreach (BorrowedMessageFragment fragment
|
||||
in borrowed.Packet.Fragments)
|
||||
{
|
||||
fragments.Add(fragment);
|
||||
}
|
||||
|
||||
Assert.Equal(packet.Fragments.Count, borrowed.Packet.FragmentCount);
|
||||
Assert.Equal(packet.Fragments.Count, fragments.Count);
|
||||
for (int index = 0; index < fragments.Count; index++)
|
||||
{
|
||||
Assert.Equal(
|
||||
packet.Fragments[index].Header,
|
||||
fragments[index].Header);
|
||||
Assert.Equal(
|
||||
packet.Fragments[index].Payload,
|
||||
fragments[index].Payload.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] ReadRetransmits(
|
||||
BorrowedOptionalHeader optional)
|
||||
{
|
||||
var retransmits = new uint[optional.RetransmitRequestCount];
|
||||
ReadOnlySpan<byte> bytes = optional.RetransmitRequestBytes.Span;
|
||||
for (int index = 0; index < retransmits.Length; index++)
|
||||
{
|
||||
retransmits[index] =
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
bytes.Slice(index * 4));
|
||||
}
|
||||
|
||||
return retransmits;
|
||||
}
|
||||
|
||||
private static void AssertSameError(byte[] datagram)
|
||||
{
|
||||
PacketCodec.PacketDecodeResult owned =
|
||||
PacketCodec.TryDecode(datagram, inboundIsaac: null);
|
||||
BorrowedPacketDecodeResult borrowed =
|
||||
PacketCodec.TryDecodeBorrowed(
|
||||
datagram,
|
||||
inboundIsaac: null);
|
||||
Assert.Equal(owned.Error, borrowed.Error);
|
||||
}
|
||||
|
||||
private static byte[] Encode(
|
||||
PacketHeaderFlags flags,
|
||||
byte[] body,
|
||||
IsaacRandom? isaac = null) =>
|
||||
PacketCodec.Encode(
|
||||
new PacketHeader
|
||||
{
|
||||
Sequence = 0x01020304,
|
||||
Flags = flags,
|
||||
Id = 0x0506,
|
||||
Time = 0x0708,
|
||||
Iteration = 0x090A,
|
||||
},
|
||||
body,
|
||||
isaac);
|
||||
|
||||
private static byte[] EncodeUnchecked(
|
||||
PacketHeaderFlags flags,
|
||||
byte[] body)
|
||||
{
|
||||
var header = new PacketHeader
|
||||
{
|
||||
Sequence = 0x01020304,
|
||||
Flags = flags,
|
||||
Id = 0x0506,
|
||||
Time = 0x0708,
|
||||
Iteration = 0x090A,
|
||||
DataSize = checked((ushort)body.Length),
|
||||
};
|
||||
header.Checksum =
|
||||
header.CalculateHeaderHash32()
|
||||
+ Hash32.Calculate(body);
|
||||
byte[] datagram = new byte[PacketHeader.Size + body.Length];
|
||||
header.Pack(datagram);
|
||||
body.CopyTo(datagram, PacketHeader.Size);
|
||||
return datagram;
|
||||
}
|
||||
|
||||
private static byte[] BuildFragment(
|
||||
uint sequence,
|
||||
ushort count,
|
||||
ushort index,
|
||||
ushort queue,
|
||||
byte[] payload)
|
||||
{
|
||||
var header = new MessageFragmentHeader
|
||||
{
|
||||
Sequence = sequence,
|
||||
Id = 0x80000000,
|
||||
Count = count,
|
||||
TotalSize = checked((ushort)(
|
||||
MessageFragmentHeader.Size + payload.Length)),
|
||||
Index = index,
|
||||
Queue = queue,
|
||||
};
|
||||
byte[] wire = new byte[header.TotalSize];
|
||||
header.Pack(wire);
|
||||
payload.CopyTo(wire, MessageFragmentHeader.Size);
|
||||
return wire;
|
||||
}
|
||||
|
||||
private static void WriteUInt16(
|
||||
Span<byte> bytes,
|
||||
ref int position,
|
||||
ushort value)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(
|
||||
bytes.Slice(position),
|
||||
value);
|
||||
position += sizeof(ushort);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(
|
||||
Span<byte> bytes,
|
||||
ref int position,
|
||||
uint value)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
bytes.Slice(position),
|
||||
value);
|
||||
position += sizeof(uint);
|
||||
}
|
||||
|
||||
private static void WriteUInt64(
|
||||
Span<byte> bytes,
|
||||
ref int position,
|
||||
ulong value)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
bytes.Slice(position),
|
||||
value);
|
||||
position += sizeof(ulong);
|
||||
}
|
||||
|
||||
private static void WriteSingle(
|
||||
Span<byte> bytes,
|
||||
ref int position,
|
||||
float value)
|
||||
{
|
||||
BinaryPrimitives.WriteSingleLittleEndian(
|
||||
bytes.Slice(position),
|
||||
value);
|
||||
position += sizeof(float);
|
||||
}
|
||||
|
||||
private static void WriteDouble(
|
||||
Span<byte> bytes,
|
||||
ref int position,
|
||||
double value)
|
||||
{
|
||||
BinaryPrimitives.WriteInt64LittleEndian(
|
||||
bytes.Slice(position),
|
||||
BitConverter.DoubleToInt64Bits(value));
|
||||
position += sizeof(double);
|
||||
}
|
||||
|
||||
private static void Fill(
|
||||
Span<byte> bytes,
|
||||
ref int position,
|
||||
int count,
|
||||
byte value)
|
||||
{
|
||||
bytes.Slice(position, count).Fill(value);
|
||||
position += count;
|
||||
}
|
||||
}
|
||||
|
|
@ -124,4 +124,81 @@ public class FragmentAssemblerTests
|
|||
assembler.DropAll();
|
||||
Assert.Equal(0, assembler.PartialCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryIngest_BorrowedSingleFragment_ReturnsOriginalMemory()
|
||||
{
|
||||
var assembler = new FragmentAssembler();
|
||||
byte[] payload = [1, 2, 3];
|
||||
var fragment = new BorrowedMessageFragment(
|
||||
MakeFrag(10, 1, 0, payload, 11).Header,
|
||||
payload);
|
||||
|
||||
bool complete = assembler.TryIngest(
|
||||
fragment,
|
||||
out ReadOnlyMemory<byte> message,
|
||||
out ushort queue);
|
||||
payload[1] = 0xAA;
|
||||
|
||||
Assert.True(complete);
|
||||
Assert.Equal(11, queue);
|
||||
Assert.Equal(0xAA, message.Span[1]);
|
||||
Assert.Equal(0, assembler.PartialCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryIngest_BorrowedMultiFragment_CopiesAcrossDatagrams()
|
||||
{
|
||||
var assembler = new FragmentAssembler();
|
||||
byte[] firstPayload = [1, 2];
|
||||
byte[] secondPayload = [3, 4];
|
||||
var first = new BorrowedMessageFragment(
|
||||
MakeFrag(20, 2, 0, firstPayload, 12).Header,
|
||||
firstPayload);
|
||||
var second = new BorrowedMessageFragment(
|
||||
MakeFrag(20, 2, 1, secondPayload, 12).Header,
|
||||
secondPayload);
|
||||
|
||||
Assert.False(assembler.TryIngest(
|
||||
first,
|
||||
out _,
|
||||
out _));
|
||||
firstPayload[0] = 0xFF;
|
||||
Assert.True(assembler.TryIngest(
|
||||
second,
|
||||
out ReadOnlyMemory<byte> message,
|
||||
out ushort queue));
|
||||
secondPayload[0] = 0xEE;
|
||||
|
||||
Assert.Equal(12, queue);
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4 }, message.ToArray());
|
||||
Assert.Equal(0, assembler.PartialCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryIngest_ConflictingBorrowedIdentity_PreservesOriginalPartial()
|
||||
{
|
||||
var assembler = new FragmentAssembler();
|
||||
var first = new BorrowedMessageFragment(
|
||||
MakeFrag(30, 2, 0, [1], 13).Header,
|
||||
new byte[] { 1 });
|
||||
var conflict = new BorrowedMessageFragment(
|
||||
MakeFrag(30, 3, 1, [9], 14).Header,
|
||||
new byte[] { 9 });
|
||||
var completion = new BorrowedMessageFragment(
|
||||
MakeFrag(30, 2, 1, [2], 13).Header,
|
||||
new byte[] { 2 });
|
||||
|
||||
Assert.False(assembler.TryIngest(first, out _, out _));
|
||||
Assert.False(assembler.TryIngest(conflict, out _, out _));
|
||||
Assert.Equal(1, assembler.PartialCount);
|
||||
Assert.True(assembler.TryIngest(
|
||||
completion,
|
||||
out ReadOnlyMemory<byte> message,
|
||||
out ushort queue));
|
||||
|
||||
Assert.Equal(13, queue);
|
||||
Assert.Equal(new byte[] { 1, 2 }, message.ToArray());
|
||||
Assert.Equal(0, assembler.PartialCount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,42 @@ public class MessageFragmentTests
|
|||
Assert.Equal(0, consumed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_ZeroFragmentCount_ReturnsNull()
|
||||
{
|
||||
var header = new MessageFragmentHeader
|
||||
{
|
||||
TotalSize = MessageFragmentHeader.Size,
|
||||
Count = 0,
|
||||
Index = 0,
|
||||
};
|
||||
byte[] buf = new byte[MessageFragmentHeader.Size];
|
||||
header.Pack(buf);
|
||||
|
||||
var (frag, consumed) = MessageFragment.TryParse(buf);
|
||||
|
||||
Assert.Null(frag);
|
||||
Assert.Equal(0, consumed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_IndexOutsideFragmentCount_ReturnsNull()
|
||||
{
|
||||
var header = new MessageFragmentHeader
|
||||
{
|
||||
TotalSize = MessageFragmentHeader.Size,
|
||||
Count = 2,
|
||||
Index = 2,
|
||||
};
|
||||
byte[] buf = new byte[MessageFragmentHeader.Size];
|
||||
header.Pack(buf);
|
||||
|
||||
var (frag, consumed) = MessageFragment.TryParse(buf);
|
||||
|
||||
Assert.Null(frag);
|
||||
Assert.Equal(0, consumed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_ConsumesExactlyOneFragment_LeavesRemainderForCaller()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue