ModernUO/Projects/Server.Tests/Tests/Serialization/BufferWriterTests.cs
2026-07-16 22:38:47 -07:00

310 lines
10 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 System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using System.Text;
using Xunit;
namespace Server.Tests;
/// <summary>
/// Pins BufferWriter's byte-level output and position semantics so the write path can be
/// optimized without behavioral drift.
/// </summary>
public class BufferWriterTests
{
[Fact]
public void PrimitivesAreLittleEndianAtExpectedOffsets()
{
var writer = new BufferWriter(new byte[256], true);
writer.Write((byte)0xAB);
writer.Write((sbyte)-5);
writer.Write(true);
writer.Write(false);
writer.Write((short)-12345);
writer.Write((ushort)54321);
writer.Write(-123456789);
writer.Write(3123456789u);
writer.Write(-1234567890123456789L);
writer.Write(12345678901234567890UL);
writer.Write(1234.5678d);
writer.Write(56.75f);
writer.Write((Serial)0x40000001u);
Assert.Equal(1 + 1 + 1 + 1 + 2 + 2 + 4 + 4 + 8 + 8 + 8 + 4 + 4, writer.Position);
var b = writer.Buffer;
Assert.Equal(0xAB, b[0]);
Assert.Equal(unchecked((byte)-5), b[1]);
Assert.Equal(1, b[2]);
Assert.Equal(0, b[3]);
Assert.Equal(-12345, BinaryPrimitives.ReadInt16LittleEndian(b.AsSpan(4)));
Assert.Equal(54321, BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(6)));
Assert.Equal(-123456789, BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(8)));
Assert.Equal(3123456789u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
Assert.Equal(-1234567890123456789L, BinaryPrimitives.ReadInt64LittleEndian(b.AsSpan(16)));
Assert.Equal(12345678901234567890UL, BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(24)));
Assert.Equal(1234.5678d, BinaryPrimitives.ReadDoubleLittleEndian(b.AsSpan(32)));
Assert.Equal(56.75f, BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(40)));
Assert.Equal(0x40000001u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(44)));
}
[Fact]
public void SeekEndUsesHighWaterMarkNotCurrentPosition()
{
var writer = new BufferWriter(new byte[256], true);
writer.Write(1L);
writer.Write(2L);
writer.Write(3L); // high water = 24
writer.Seek(4, SeekOrigin.Begin);
writer.Write(99); // position now 8, high water still 24
Assert.Equal(24, writer.Seek(0, SeekOrigin.End));
Assert.Equal(24, writer.Position);
}
[Fact]
public void SeekCurrentAndBeginBehave()
{
var writer = new BufferWriter(new byte[64], true);
writer.Write(0xDEADBEEF);
Assert.Equal(2, writer.Seek(2, SeekOrigin.Begin));
Assert.Equal(3, writer.Seek(1, SeekOrigin.Current));
writer.Write((byte)0x77);
Assert.Equal(0x77, writer.Buffer[3]);
}
[Fact]
public void GrowthPreservesContentAndPosition()
{
var writer = new BufferWriter(new byte[16], true);
for (var i = 0; i < 100; i++)
{
writer.Write((long)i);
}
Assert.Equal(800, writer.Position);
Assert.True(writer.Buffer.Length >= 800);
for (var i = 0; i < 100; i++)
{
Assert.Equal(i, BinaryPrimitives.ReadInt64LittleEndian(writer.Buffer.AsSpan(i * 8)));
}
}
[Fact]
public void SpanWriteCrossesGrowthBoundary()
{
var writer = new BufferWriter(new byte[8], true);
Span<byte> payload = stackalloc byte[64];
for (var i = 0; i < payload.Length; i++)
{
payload[i] = (byte)(i + 1);
}
writer.Write((ushort)7);
writer.Write(payload);
Assert.Equal(66, writer.Position);
Assert.Equal(payload.ToArray(), writer.Buffer.AsSpan(2, 64).ToArray());
}
[Theory]
[InlineData(0, new byte[] { 0x00 })]
[InlineData(127, new byte[] { 0x7F })]
[InlineData(128, new byte[] { 0x80, 0x01 })]
[InlineData(0x3FFF, new byte[] { 0xFF, 0x7F })]
[InlineData(0x4000, new byte[] { 0x80, 0x80, 0x01 })]
[InlineData(0x1F_FFFF, new byte[] { 0xFF, 0xFF, 0x7F })]
[InlineData(0x20_0000, new byte[] { 0x80, 0x80, 0x80, 0x01 })]
[InlineData(0xFFF_FFFF, new byte[] { 0xFF, 0xFF, 0xFF, 0x7F })]
[InlineData(0x1000_0000, new byte[] { 0x80, 0x80, 0x80, 0x80, 0x01 })]
[InlineData(int.MaxValue, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x07 })]
[InlineData(-1, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F })]
public void EncodedIntMatchesFormat(int value, byte[] expected)
{
var writer = new BufferWriter(new byte[16], true);
((IGenericWriter)writer).WriteEncodedInt(value);
Assert.Equal(expected.Length, writer.Position);
Assert.Equal(expected, writer.Buffer.AsSpan(0, expected.Length).ToArray());
}
[Fact]
public void PrefixedStringsWriteFlagLengthAndUtf8()
{
var writer = new BufferWriter(new byte[256], true);
writer.Write("héllo Ωorld");
var utf8 = Encoding.UTF8.GetBytes("héllo Ωorld");
var b = writer.Buffer;
Assert.Equal(1, b[0]); // not-null flag
Assert.Equal(utf8.Length, b[1]); // encoded length (small string = 1 byte)
Assert.Equal(utf8, b.AsSpan(2, utf8.Length).ToArray());
Assert.Equal(2 + utf8.Length, writer.Position);
writer.Write((string)null);
Assert.Equal(0, b[2 + utf8.Length]); // null flag
}
[Theory]
[InlineData(84)] // scratch path
[InlineData(85)] // scratch path boundary
[InlineData(86)] // two-pass path
[InlineData(300)] // two-pass, length prefix > 1 byte
public void StringPathsAgreeAcrossTheScratchBoundary(int chars)
{
// Mixed ASCII, 2-byte, 3-byte, and surrogate-pair (4-byte) content
var builder = new StringBuilder(chars);
for (var i = 0; builder.Length < chars; i++)
{
switch (i % 4)
{
case 0:
builder.Append('a');
break;
case 1:
builder.Append('é');
break;
case 2:
builder.Append('Ω');
break;
default:
if (builder.Length + 2 <= chars)
{
builder.Append("𝔘"); // surrogate pair
}
else
{
builder.Append('z');
}
break;
}
}
var value = builder.ToString();
Assert.Equal(chars, value.Length);
var writer = new BufferWriter(new byte[16], true); // forces growth through both paths
writer.Write(value);
var utf8 = Encoding.UTF8.GetBytes(value);
var b = writer.Buffer;
Assert.Equal(1, b[0]);
// decode the 7-bit encoded length prefix
var offset = 1;
var length = 0;
var shift = 0;
byte current;
do
{
current = b[offset++];
length |= (current & 0x7F) << shift;
shift += 7;
} while ((current & 0x80) != 0);
Assert.Equal(utf8.Length, length);
Assert.Equal(utf8, b.AsSpan(offset, length).ToArray());
Assert.Equal(offset + length, writer.Position);
}
[Fact]
public void DateTimeWritesUtcTicksViaInterface()
{
var writer = new BufferWriter(new byte[64], true);
IGenericWriter iface = writer;
var utc = new DateTime(2026, 7, 13, 1, 2, 3, DateTimeKind.Utc);
var local = utc.ToLocalTime();
iface.Write(utc);
iface.Write(local); // must convert to UTC
Assert.Equal(utc.Ticks, BinaryPrimitives.ReadInt64LittleEndian(writer.Buffer.AsSpan(0)));
Assert.Equal(utc.Ticks, BinaryPrimitives.ReadInt64LittleEndian(writer.Buffer.AsSpan(8)));
}
[Fact]
public void Point3DWritesThreeInts()
{
var writer = new BufferWriter(new byte[64], true);
IGenericWriter iface = writer;
iface.Write(new Point3D(100, -200, 30));
Assert.Equal(12, writer.Position);
Assert.Equal(100, BinaryPrimitives.ReadInt32LittleEndian(writer.Buffer.AsSpan(0)));
Assert.Equal(-200, BinaryPrimitives.ReadInt32LittleEndian(writer.Buffer.AsSpan(4)));
Assert.Equal(30, BinaryPrimitives.ReadInt32LittleEndian(writer.Buffer.AsSpan(8)));
}
[Fact]
public void DecimalRoundTripsThroughReader()
{
var writer = new BufferWriter(new byte[64], true);
writer.Write(1234567.89012m);
IGenericReader reader = new BufferReader(writer.Buffer);
Assert.Equal(1234567.89012m, reader.ReadDecimal());
}
[Fact]
public void LongStringPrefixIsZeroPaddedToWorstCaseWidth()
{
// 100 ASCII chars: worst case 300 bytes -> 2-byte prefix; actual 100 bytes would
// canonically fit in 1. The prefix must be the non-minimal 2-byte form the readers
// decode identically: (100 | 0x80), 0x00.
var value = new string('a', 100);
var writer = new BufferWriter(new byte[1024], false);
writer.WriteRaw(value);
var b = writer.Buffer;
Assert.Equal((byte)(100 | 0x80), b[0]);
Assert.Equal(0, b[1]);
Assert.Equal(2 + 100, writer.Position);
IGenericReader reader = new BufferReader(writer.Buffer);
Assert.Equal(value, reader.ReadStringRaw());
}
[Theory]
[InlineData(42)] // canonical 1-byte prefix (3 * 42 < 0x80)
[InlineData(100)] // padded prefix
[InlineData(10_000)] // multi-byte prefix, forces growth from a small buffer
public void ThreeBytePerCharContentRoundTrips(int chars)
{
// CJK content encodes at the UTF-8 worst case of 3 bytes per char - the case an
// undersized scratch would truncate or throw on.
var value = new string('二', chars);
var writer = new BufferWriter(new byte[16], true);
writer.Write(value);
Assert.Equal(Encoding.UTF8.GetByteCount(value), 3 * chars);
IGenericReader reader = new BufferReader(writer.Buffer);
Assert.Equal(value, reader.ReadString());
}
[Fact]
public void SurrogatePairContentRoundTripsThroughRawPath()
{
// Surrogate pairs encode 2 chars into 4 bytes (2 bytes per char) - under the
// 3-bytes-per-char reservation, exercising written < maxLength with a padded prefix.
var value = string.Concat(Enumerable.Repeat("😀", 60)); // 120 chars, 240 bytes
var writer = new BufferWriter(new byte[16], false);
writer.WriteRaw(value);
IGenericReader reader = new BufferReader(writer.Buffer);
Assert.Equal(value, reader.ReadStringRaw());
}
}