using System; using System.Buffers; using System.Diagnostics; using System.Net.Sockets; using System.Network; using System.Runtime.CompilerServices; using System.Threading; using Server.Network; using Xunit; namespace Server.Tests.Network; /// /// Shared plumbing for tests that drive a real over loopback. /// public static class NetTestHelpers { public const byte PingPacketId = 0x73; /// The payload byte of the last ping delivered to the handler from . public static byte LastPing { get; private set; } /// /// Restores the handler that was in a packet slot before a test replaced it. The handler table is /// process-global, so a handler left behind would silently answer for that id in every later test. /// public readonly struct HandlerRegistration : IDisposable { private readonly int _packetId; private readonly PacketHandler _previous; internal HandlerRegistration(int packetId, PacketHandler previous) { _packetId = packetId; _previous = previous; } public void Dispose() => IncomingPackets.Handlers[_packetId] = _previous; } public static unsafe HandlerRegistration RegisterHandler( int packetId, int length, delegate* onReceive ) { var previous = IncomingPackets.GetHandler(packetId); IncomingPackets.Register(packetId, length, false, onReceive); return new HandlerRegistration(packetId, previous); } /// Registers a two-byte ping whose payload byte is recorded in . public static unsafe HandlerRegistration RegisterPing() { LastPing = 0; return RegisterHandler(PingPacketId, 2, &RecordPing); } private static void RecordPing(NetState state, SpanReader reader) => LastPing = reader.ReadByte(); public static byte[] Pattern(int length, int seed) { var data = new byte[length]; new System.Random(seed).NextBytes(data); return data; } /// /// Pre-auth buffers are off only where the platform floor leaves no room below the base size /// (the Windows Server 2012/2016 mapping path, or a 64 KiB-page kernel). Anywhere else a zero /// initial size is a configuration regression and fails rather than skips. /// public static void RequirePreAuthBuffers() { var floor = IORingBuffer.MinimumSize; Skip.If( floor >= NetState.RecvBufferSize || floor >= NetState.SendBufferSize, $"Pre-auth buffers are off on this platform: the {floor}-byte mapping floor leaves no room below the base buffers (recv {NetState.RecvBufferSize}, send {NetState.SendBufferSize})" ); Assert.True( NetState.InitialRecvBufferSize > 0 && NetState.InitialSendBufferSize > 0, $"Pre-auth buffers are off although the {floor}-byte platform floor allows them (initial recv {NetState.InitialRecvBufferSize}, send {NetState.InitialSendBufferSize})" ); } /// Runs the network slice until holds, failing with the condition that timed out. public static void SliceUntil( Func done, int timeoutMs = 5000, [CallerArgumentExpression(nameof(done))] string condition = null ) { var deadline = Stopwatch.StartNew(); while (!done()) { if (deadline.ElapsedMilliseconds >= timeoutMs) { Assert.Fail($"Timed out after {timeoutMs} ms waiting for {condition}"); } NetState.Slice(); Thread.Sleep(5); } } /// Reads exactly bytes from the peer, slicing the server while it waits. public static byte[] ReadAll(Socket client, int length, int timeoutMs = 10000) { var received = new byte[length]; var total = 0; var deadline = Stopwatch.StartNew(); while (total < length) { if (deadline.ElapsedMilliseconds >= timeoutMs) { Assert.Fail($"Timed out after {timeoutMs} ms with {total} of {length} bytes received by the peer"); } NetState.Slice(); // Poll takes microseconds, not milliseconds if (client.Poll(50_000, SelectMode.SelectRead)) { var read = client.Receive(received, total, length - total, SocketFlags.None); Assert.True(read != 0, $"Peer saw EOF after {total} of {length} bytes"); total += read; } } return received; } /// Waits for the server's FIN to reach the peer, failing if any data arrives first. public static void WaitForEof(Socket client, int timeoutMs = 10000) { var scratch = new byte[64]; var deadline = Stopwatch.StartNew(); while (true) { if (deadline.ElapsedMilliseconds >= timeoutMs) { Assert.Fail($"Timed out after {timeoutMs} ms waiting for the peer to see EOF"); } NetState.Slice(); if (client.Poll(50_000, SelectMode.SelectRead)) { var read = client.Receive(scratch); Assert.True(read == 0, $"Peer received {read} unexpected bytes while waiting for EOF"); return; } } } }