解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
9.99 KiB
X
using System.Net;
using System.Net.Sockets;
using NewLife;
using NewLife.Data;
using NewLife.Net;
using Xunit;

namespace XUnitTest.Net;

/// <summary>接收错误处理契约</summary>
[Collection("Integration")]
public class ReceiveErrorTests
{
    /// <summary>探针服务器:暴露 protected 的接收槽释放入口</summary>
    private sealed class ProbeUdpServer : UdpServer
    {
        public void Release(SocketAsyncEventArgs se, String reason) => ReleaseRecv(se, reason);
    }
    [Fact(DisplayName = "接收错误_连接型会话非ConnectionReset错误_也关闭会话并通知")]
    public void ReceiveError_StreamSession_ClosesOnAnyError()
    {
        var ready = new ManualResetEventSlim(false);
        var closed = new ManualResetEventSlim(false);
        SessionBase? ss = null;

        using var server = new NetServer
        {
            Port = 0,
            ProtocolType = NetType.Tcp,
            AddressFamily = AddressFamily.InterNetwork,
        };
        server.NewSession += (s, e) =>
        {
            ss = e.Session?.Session as SessionBase;
            if (ss != null) ss.Closed += (a, b) => closed.Set();

            ready.Set();
        };
        server.Start();

        using var client = new NetClient($"tcp://127.0.0.1:{server.Port}") { AutoReconnect = false };
        Assert.True(client.Open());
        Assert.True(ready.Wait(3000));
        Assert.NotNull(ss);

        // NetworkDown 与 ConnectionReset 一样说明连接不可用。只对 ConnectionReset 关闭会留下半死会话:
        // Active 仍为 true、不再收数据、也不触发 Closed,上层自动重连永不启动
        var se = new SocketAsyncEventArgs { SocketError = SocketError.NetworkDown };
        Assert.True(ss!.OnReceiveError(se));

        Assert.True(closed.Wait(3000));
        Assert.False(ss.Active);
    }

    [Fact(DisplayName = "UDP接收错误_报文级错误_不销毁接收槽")]
    public void UdpReceiveError_KeepsSlot()
    {
        using var server = new UdpServer { Port = 0, MaxAsync = 2 };
        server.Open();

        // MessageSize(报文超出接收缓冲)此前返回 true,会让 ProcessEvent 释放该接收槽;
        // 槽位只在启动时创建一次、销毁后不重建,累计耗尽后整台服务器静默停收
        var size = new SocketAsyncEventArgs
        {
            SocketError = SocketError.MessageSize,
            RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 12345),
        };
        Assert.False(server.OnReceiveError(size));

        // Reset/Aborted 只关对应会话,同样销毁接收槽
        var reset = new SocketAsyncEventArgs
        {
            SocketError = SocketError.ConnectionReset,
            RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 12345),
        };
        Assert.False(server.OnReceiveError(reset));

        Assert.True(server.Active);
    }

    [Fact(DisplayName = "UDP接收_超大报文触发MessageSize_服务仍可收包")]
    public async Task UdpMessageSize_KeepsReceiving()
    {
        // 接收缓冲刻意调小以迫使大报文触发 MessageSize;MaxAsync=2,旧实现下两次错误即耗尽全部接收槽,
        // 服务随后静默失聪(后续报文一律收不到)
        using var server = new UdpServer { Port = 0, BufferSize = 1024, MaxAsync = 2 };
        server.Received += (s, e) =>
        {
            if (s is UdpSession session && e.Packet != null && e.Packet.Length > 0) session.Send(e.Packet);
        };
        server.Open();

        var remote = new IPEndPoint(IPAddress.Loopback, server.Port);

        // 连发多个超出接收缓冲的报文;其回显可能被截断,本用例不关心
        using (var flood = NetHelper.CreateUdpClient())
        {
            var big = new Byte[2048];
            for (var i = 0; i < 6; i++) await flood.SendAsync(big, big.Length, remote);

            await Task.Delay(200);
        }

        // 换一个客户端(新远端端点)发普通报文,必须仍能被接收并回显
        using var client = NetHelper.CreateUdpClient();
        var payload = new Byte[] { 9, 8, 7, 6 };

        // UDP 在并行满载时可能丢包,重发直到收到回显
        Byte[]? result = null;
        var end = Environment.TickCount64 + 10_000;
        while (result == null && Environment.TickCount64 < end)
        {
            await client.SendAsync(payload, payload.Length, remote);

            try
            {
                var rs = await client.ReceiveAsync().WaitAsync(TimeSpan.FromMilliseconds(500));
                result = rs.Buffer;
            }
            catch (TimeoutException) { }
        }

        Assert.NotNull(result);
        Assert.Equal(payload, result);
    }

    [Fact(DisplayName = "接收槽释放_同一事件参数重复释放_只生效一次")]
    public void ReleaseRecv_IsIdempotent()
    {
        using var server = new ProbeUdpServer { Port = 0, MaxAsync = 2 };
        server.Open();
        Assert.True(server.IsReceiving);

        // 模拟 StartReceive 抛 ObjectDisposedException 冒泡回 ProcessEvent 的 catch 再次释放的路径
        var owner = new OwnerPacket(16);
        var se = new SocketAsyncEventArgs { UserToken = new RecvSlot(100, owner) };
        server.Release(se, "first");
        server.Release(se, "second");

        // 只递减一次:另一个接收槽仍在收。重复释放两次会让计数归零,拉取直读的互斥判定随之失效
        Assert.True(server.IsReceiving);
        Assert.Throws<ObjectDisposedException>(() => owner.Buffer);
    }

    [Fact(DisplayName = "接收槽释放_事件参数无接收槽_抛异常")]
    public void ReleaseRecv_MissingSlot_Throws()
    {
        using var server = new ProbeUdpServer { Port = 0, MaxAsync = 2 };
        server.Open();

        // 接收事件参数一律由 StartReceive 创建并挂接收槽;裸事件参数属契约破坏,直接抛错暴露
        var se = new SocketAsyncEventArgs();
        Assert.Throws<InvalidOperationException>(() => server.Release(se, "no-slot"));

        // 拒绝释放不影响其它接收槽
        Assert.True(server.IsReceiving);
    }

    [Fact(DisplayName = "接收槽释放_首释槽中无句柄_抛异常")]
    public void ReleaseRecv_MissingPacket_Throws()
    {
        using var server = new ProbeUdpServer { Port = 0, MaxAsync = 2 };
        server.Open();

        // 槽中句柄随缓冲借出时创建、此后一直挂槽;首释即缺失说明契约被破坏(如轮末换新缓冲失败)
        var orphan = new OwnerPacket(16);
        var se = new SocketAsyncEventArgs { UserToken = new RecvSlot(0, orphan) { Packet = null } };
        Assert.Throws<InvalidOperationException>(() => server.Release(se, "no-packet"));

        Assert.True(server.IsReceiving);

        orphan.Dispose();
    }

    [Fact(DisplayName = "接收槽释放_计数归零_释放并置空套接字")]
    public void ReleaseRecv_CountReachesZero_ReleasesClient()
    {
        using var server = new ProbeUdpServer { Port = 0, MaxAsync = 2 };
        server.Open();
        Assert.True(server.IsReceiving);

        var socket = server.Client;
        Assert.NotNull(socket);

        // 释放全部接收槽:计数归零代表接收环彻底停止,套接字应被释放并置空
        server.Release(new SocketAsyncEventArgs { UserToken = new RecvSlot(0, new OwnerPacket(16)) }, "drain-1");
        server.Release(new SocketAsyncEventArgs { UserToken = new RecvSlot(1, new OwnerPacket(16)) }, "drain-2");

        Assert.False(server.IsReceiving);
        Assert.Null(server.Client);
        Assert.True(socket.SafeHandle.IsClosed);

        // 计数已归零后的多余释放:不抛异常,也不影响已收口状态
        server.Release(new SocketAsyncEventArgs { UserToken = new RecvSlot(2, new OwnerPacket(16)) }, "drain-again");
        Assert.Null(server.Client);
    }

    [Fact(DisplayName = "接收槽释放_缓冲仍被共享切片持有_只释放自身引用不归还")]
    public void ReleaseRecv_SharedSlice_DoesNotReturnBuffer()
    {
        using var server = new ProbeUdpServer { Port = 0 };

        var owner = new OwnerPacket(64);
        var view = owner.Slice(0, 16);
        Assert.Equal(2, view.RefCount);

        var se = new SocketAsyncEventArgs { UserToken = new RecvSlot(0, owner) };
        server.Release(se, "shared");

        // 缓冲仍在被共享切片使用:本句柄只释放自己的引用,缓冲留给最后一个句柄归还。
        // 旧实现在该分支 Detach 抛异常被静默吞掉,本句柄的引用从未释放,池缓冲永久漏出
        Assert.Equal(1, view.RefCount);
        Assert.Equal(16, view.Length);

        view.Dispose();
    }

    [Fact(DisplayName = "接收槽释放_独占槽句柄_释放句柄自身")]
    public void ReleaseRecv_SoleHandle_DisposesHandle()
    {
        using var server = new ProbeUdpServer { Port = 0 };

        var owner = new OwnerPacket(64);
        Assert.Equal(1, owner.RefCount);

        // 接收槽句柄随缓冲借出时创建,是缓冲的唯一持有者
        var se = new SocketAsyncEventArgs { UserToken = new RecvSlot(0, owner) };
        server.Release(se, "sole");

        // 释放槽句柄即完成归还:引用归零、缓冲随 ArrayOwner 回池(不再 Detach 后手工归还)
        Assert.Equal(0, owner.RefCount);
        Assert.Throws<ObjectDisposedException>(() => owner.Buffer);
    }

    [Fact(DisplayName = "接收槽释放_槽句柄带链_连带释放链上各段")]
    public void ReleaseRecv_ChainHandle_DisposesWholeChain()
    {
        using var server = new ProbeUdpServer { Port = 0 };

        var head = new OwnerPacket(64);
        var tail = new OwnerPacket(32);
        head.Next = tail;

        var se = new SocketAsyncEventArgs { UserToken = new RecvSlot(0, head) };
        server.Release(se, "chain");

        // 槽句柄带链时同样直接释放:头节点归零归还缓冲,链上各段随 Dispose 递归释放,无需单独判别 Next
        Assert.Throws<ObjectDisposedException>(() => head.Buffer);
        Assert.Throws<ObjectDisposedException>(() => tail.Buffer);
    }
}