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

namespace XUnitTest.Net;

/// <summary>会话生命周期权威契约测试</summary>
/// <remarks>
/// 会话“还在不在、活没活”同时记在多处状态里,本组用例把《网络库架构》§10 定下的口径钉住:
/// Disposed 是终态权威、Sessions 是存在性权威、Active 只表示传输可用;
/// “Active=false 但未释放、仍在集合里”只能是必然收敛的瞬态。
/// </remarks>
[Collection("Net.B")]
public class SessionAuthorityContractTests
{
    #region 工具
    /// <summary>等待条件成立,超时抛异常</summary>
    private static async Task WaitUntilAsync(Func<Boolean> condition, Int32 timeoutMs = 10_000)
    {
        var sw = Stopwatch.StartNew();
        while (!condition())
        {
            if (sw.ElapsedMilliseconds > timeoutMs) throw new TimeoutException("等待条件超时");

            await Task.Delay(10);
        }
    }
    #endregion

    /// <summary>服务端会话由宿主接管:构造时即视为活动,Open 幂等成功(不执行打开流程)</summary>
    [Fact(DisplayName = "生命周期权威_服务端会话构造即为活动_打开幂等成功")]
    public async Task ServerSession_ActiveOnConstruction_OpenIdempotent()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        ISocketSession? session = null;
        server.NewSession += (s, e) => session = e.Session;
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => session != null);

        var tcp = Assert.IsType<TcpSession>(session);
        Assert.True(tcp.Active, "服务端会话在构造时即置为活动(表示连接已被接受)");

        // Active 已为 true,打开会直接短路;若真进了 OnOpenAsync,服务端会话会返回 false
        Assert.True(tcp.Open(), "服务端会话的打开是幂等成功");
        Assert.True(tcp.Active);
    }

    /// <summary>服务端会话关闭后必须收敛为已释放并移出会话集合</summary>
    [Fact(DisplayName = "生命周期权威_服务端会话释放后_必出会话集合")]
    public async Task ServerSession_Disposed_RemovedFromCollection()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        ISocketSession? session = null;
        server.NewSession += (s, e) => session = e.Session;
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => session != null && server.Sessions.Count == 1);

        var tcp = Assert.IsType<TcpSession>(session);

        // 关闭过程中允许出现“活动为假、尚未释放、仍在集合里”的瞬态,但必须收敛
        tcp.Close("契约用例");

        await WaitUntilAsync(() => tcp.Disposed && server.Sessions.Count == 0);

        Assert.True(tcp.Disposed);
        Assert.False(tcp.Active);
        Assert.Empty(server.Sessions);
    }

    /// <summary>会话集合只收未释放会话,已释放的会话不得再被登记</summary>
    [Fact(DisplayName = "生命周期权威_已释放会话_不再被会话集合接受")]
    public async Task DisposedSession_RejectedByCollection()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        ISocketSession? session = null;
        server.NewSession += (s, e) => session = e.Session;
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => session != null && server.Sessions.Count == 1);

        var tcp = Assert.IsType<TcpSession>(session);
        tcp.Close("契约用例");
        await WaitUntilAsync(() => tcp.Disposed && server.Sessions.Count == 0);

        var sessions = (SessionCollection)server.Sessions;
        Assert.False(sessions.Add(tcp), "已释放的会话不应被重新登记");
        Assert.Empty(sessions);
    }

    /// <summary>底层连接不可用时,服务端会话仍必须释放并出集合,不能留在集合里等超时清理</summary>
    /// <remarks>实测该场景不会进入 OnCloseAsync 的异常分支(接收环错误路径先一步收敛),
    /// 因此本用例锁定的是“无论如何都会收敛到已释放”的最终契约,无负向验证</remarks>
    [Fact(DisplayName = "生命周期权威_关闭动作失败_服务端会话仍被释放")]
    public async Task ServerSession_CloseFails_StillDisposed()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        ISocketSession? session = null;
        server.NewSession += (s, e) => session = e.Session;
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => session != null && server.Sessions.Count == 1);

        var tcp = Assert.IsType<TcpSession>(session);

        // 制造“关闭动作无法成功”的现场:先把底层套接字释放
        tcp.Client!.Dispose();

        tcp.Close("契约用例-关闭失败");

        // 无论关闭动作是否成功,服务端会话都必须收敛到终态并移出集合
        await WaitUntilAsync(() => tcp.Disposed && server.Sessions.Count == 0);

        Assert.True(tcp.Disposed);
        Assert.False(tcp.Active);
        Assert.Empty(server.Sessions);
    }

    /// <summary>对端 RST 断开时,服务端会话必须及时释放并出集合(不能只靠会话超时清理兜底)</summary>
    [Fact(DisplayName = "生命周期权威_对端重置连接_服务端会话及时释放")]
    public async Task ServerSession_PeerReset_SessionReleased()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        ISocketSession? session = null;
        server.NewSession += (s, e) => session = e.Session;
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => session != null && server.Sessions.Count == 1);

        var tcp = Assert.IsType<TcpSession>(session);

        // LingerState(true, 0):客户端关闭时直接发 RST 而不是 FIN,服务端接收环随即报 ConnectionReset
        client.LingerState = new LingerOption(true, 0);
        client.Close();

        // 不缩短 SessionTimeout,用等待窗口区分“立即释放”与“等超时清理”
        await WaitUntilAsync(() => tcp.Disposed && server.Sessions.Count == 0, 5_000);

        Assert.True(tcp.Disposed);
        Assert.False(tcp.Active);
        Assert.Empty(server.Sessions);
    }

    /// <summary>登记会话时,会话数与集合数一致;客户端断开后两者一起归零</summary>
    [Fact(DisplayName = "生命周期权威_登记会话时_会话数与集合数一致")]
    public async Task NetServer_SessionCount_MatchesCollection()
    {
        using var server = new NetServer
        {
            Port = 0,
            ProtocolType = NetType.Tcp,
            UseSession = true,
            Log = XTrace.Log,
        };
        server.Start();

        var client1 = new TcpClient();
        var client2 = new TcpClient();
        try
        {
            await client1.ConnectAsync(IPAddress.Loopback, server.Port);
            await client2.ConnectAsync(IPAddress.Loopback, server.Port);

            await WaitUntilAsync(() => server.SessionCount == 2 && server.Sessions.Count == 2);

            Assert.Equal(2, server.SessionCount);
            Assert.Equal(2, server.Sessions.Count);

            client1.Close();
            client2.Close();

            await WaitUntilAsync(() => server.SessionCount == 0 && server.Sessions.Count == 0);

            Assert.Equal(0, server.SessionCount);
            Assert.Empty(server.Sessions);
        }
        finally
        {
            client1.Dispose();
            client2.Dispose();
        }
    }

    /// <summary>不登记会话时集合恒空,但会话计数仍反映真实会话数</summary>
    [Fact(DisplayName = "生命周期权威_不登记会话时_集合为空但计数仍准确")]
    public async Task NetServer_UseSessionFalse_CountStillAccurate()
    {
        using var server = new NetServer
        {
            Port = 0,
            ProtocolType = NetType.Tcp,
            UseSession = false,
            Log = XTrace.Log,
        };
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => server.SessionCount == 1);

        // 计数权威是 _SessionCount 计数器,与是否登记进会话集合无关
        Assert.Equal(1, server.SessionCount);
        Assert.Empty(server.Sessions);

        client.Close();

        await WaitUntilAsync(() => server.SessionCount == 0);
        Assert.Equal(0, server.SessionCount);
    }

    /// <summary>统计输出不得反写会话计数:不登记会话(UseSession=false)时字典恒空,不能把计数清零</summary>
    [Fact(DisplayName = "生命周期权威_统计输出不反写会话计数")]
    public async Task NetServer_GetStat_DoesNotOverwriteSessionCount()
    {
        using var server = new NetServer
        {
            Port = 0,
            ProtocolType = NetType.Tcp,
            UseSession = false,
            Log = XTrace.Log,
        };
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => server.SessionCount == 1);
        Assert.Empty(server.Sessions);

        var stat = server.GetStat();
        Assert.False(stat.IsNullOrEmpty(), "有会话时统计输出不应为空");

        // 权威计数是 _SessionCount 计数器,不因统计输出而改变
        Assert.Equal(1, server.SessionCount);
    }
}