v9.10.2019.0101  全面巩固批量Insert/Update/Upsert,支持数据备份、恢复和同步,支持实体列表保存到文件以及加载
大石头 authored at 2019-01-01 13:38:33
12.07 KiB
X
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text;
using NewLife;
using NewLife.Data;
using NewLife.Log;
using NewLife.Net;
using Xunit;

namespace XUnitTest.Integration;

/// <summary>NetworkServer 集成测试固定装置,复现 Samples/Zero.Server 的逻辑</summary>
public class NetworkServerFixture : IDisposable
{
    /// <summary>网络服务端实例(强类型,便于访问 ReceivedMessages 与 ConnectionCount)</summary>
    public MyNetServer Server { get; }

    /// <summary>服务端日志捕获器,用于断言连接/接收/断开等服务端行为</summary>
    public TestLog ServerLog { get; } = new();

    public NetworkServerFixture()
    {
        var server = new MyNetServer
        {
            Port = 0,
            AddressFamily = AddressFamily.InterNetwork,
            Log = ServerLog,
            SessionLog = ServerLog,
        };
        Server = server;
        Server.Start();
    }

    public void Dispose() => Server?.Stop("IntegrationTestDone");
}

/// <summary>网络服务端,追踪累计连接数与服务端收到的消息内容</summary>
public class MyNetServer : NetServer<MyNetSession>
{
    /// <summary>已建立的累计连接数(线程安全)</summary>
    public Int32 ConnectionCount;

    /// <summary>服务端收到的原始消息内容,线程安全</summary>
    public ConcurrentQueue<String> ReceivedMessages { get; } = new();
}

/// <summary>定义会话。连接时发欢迎语,收到数据后:记录到服务端队列、写日志、返回反转字符串</summary>
public class MyNetSession : NetSession<MyNetServer>
{
    protected override void OnConnected()
    {
        Interlocked.Increment(ref Host.ConnectionCount);
        Send($"Welcome to visit {Environment.MachineName}!  [{Remote}]\r\n");
        base.OnConnected();
    }

    protected override void OnDisconnected(String reason)
    {
        WriteLog("客户端 {0} 已断开连接。{1}", Remote, reason);
        base.OnDisconnected(reason);
    }

    protected override void OnReceive(ReceivedEventArgs e)
    {
        var packet = e.Packet;
        if (packet == null || packet.Length == 0) return;

        var msg = packet.ToStr();
        Host.ReceivedMessages.Enqueue(msg);     // 服务端跟踪,供测试断言
        WriteLog("收到:{0}", msg);              // 触发 TestLog 记录
        Send(msg.Reverse().Join(null));
    }

    protected override void OnError(Object sender, ExceptionEventArgs e)
    {
        WriteLog("[{0}] 错误:{1}", e.Action, e.Exception?.GetTrue().Message);
        base.OnError(sender, e);
    }
}

/// <summary>NetworkServer 基础集成测试,验证 TCP/UDP 连接与收发功能</summary>
[Collection("Integration")]
[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
public class NetworkServerIntegrationTests(NetworkServerFixture fixture) : IClassFixture<NetworkServerFixture>
{
    [Fact(DisplayName = "01-服务端已启动且端口已分配")]
    public void Test01_ServerStarted()
    {
        Assert.True(fixture.Server.Active, "服务端应处于运行状态");
        Assert.True(fixture.Server.Port > 0, "端口应已分配");

        XTrace.WriteLine("NetworkServer 已在端口 {0} 上启动", fixture.Server.Port);
    }

    [Fact(DisplayName = "02-TcpClient 全流程:连接→欢迎→发送→精确回显→服务端确认")]
    public async Task Test02_TcpClient_FullFlow()
    {
        var port = fixture.Server.Port;
        var priorCount = fixture.Server.ReceivedMessages.Count;
        var sessionsBefore = fixture.Server.SessionCount;
        var maxBefore = fixture.Server.MaxSessionCount;

        var client = new TcpClient();
        await client.ConnectAsync("127.0.0.1", port);
        var ns = client.GetStream();
        ns.ReadTimeout = 5_000;

        // 等待服务端完成会话建立(轮询避免偶发时序问题)
        for (var i = 0; i < 20 && fixture.Server.SessionCount < sessionsBefore + 1; i++)
            await Task.Delay(50);

        // 连接后:SessionCount 精确 +1;MaxSessionCount 同步更新(只增不减)
        Assert.Equal(sessionsBefore + 1, fixture.Server.SessionCount);
        var expectedMax = Math.Max(maxBefore, sessionsBefore + 1);
        Assert.Equal(expectedMax, fixture.Server.MaxSessionCount);

        // 1. 接收欢迎消息,精确验证整句结构(不多不少)
        //    格式:$"Welcome to visit {MachineName}!  [tcp://{ip}:{port}]\r\n"
        var buf = new Byte[1024];
        var n = await ns.ReadAsync(buf);
        var welcome = Encoding.UTF8.GetString(buf, 0, n);
        var expectedStart = $"Welcome to visit {Environment.MachineName}!  [tcp://127.0.0.1:";
        Assert.True(welcome.StartsWith(expectedStart, StringComparison.Ordinal),
            $"欢迎词应以 '{expectedStart}' 开头,实际:'{welcome}'");
        Assert.True(welcome.EndsWith("]\r\n", StringComparison.Ordinal),
            $"欢迎词应以 ']\\r\\n' 结尾,实际:'{welcome}'");
        var portPart = welcome[expectedStart.Length..welcome.IndexOf("]\r\n")];
        Assert.True(Int32.TryParse(portPart, out var clientPort) && clientPort is > 0 and <= 65535,
            $"欢迎词中端口号应为有效端口(1-65535),实际:'{portPart}'");

        // 2. 发送消息
        const String msg = "Hello NewLife";
        await ns.WriteAsync(Encoding.UTF8.GetBytes(msg));

        // 3. 接收回显,精确验证反转结果
        n = await ns.ReadAsync(buf);
        var reply = Encoding.UTF8.GetString(buf, 0, n);
        Assert.Equal("efiLweN olleH", reply);

        // 4. 服务端日志确认:包含接收记录
        Assert.True(await fixture.ServerLog.WaitForAsync("收到"), "服务端日志应包含接收记录");

        // 5. 服务端队列:精确多了1条(不是「大于」)
        Assert.Equal(priorCount + 1, fixture.Server.ReceivedMessages.Count);
        Assert.Contains(msg, fixture.Server.ReceivedMessages);

        // 主动关闭连接,等待服务端清理会话
        client.Close();
        client.Dispose();
        await Task.Delay(150);

        // 断开后:SessionCount 精确回到原值;MaxSessionCount 保持不减
        Assert.Equal(sessionsBefore, fixture.Server.SessionCount);
        Assert.Equal(expectedMax, fixture.Server.MaxSessionCount);
    }

    [Fact(DisplayName = "03-UdpClient 全流程:发包→欢迎→精确回显→服务端确认")]
    public async Task Test03_UdpClient_FullFlow()
    {
        var port = fixture.Server.Port;
        var endpoint = new IPEndPoint(IPAddress.Loopback, port);
        const String msg = "Hello NewLife";
        var msgBytes = Encoding.UTF8.GetBytes(msg);
        var sessionsBefore = fixture.Server.SessionCount;
        var maxBefore = fixture.Server.MaxSessionCount;

        using var udp = new UdpClient();
        udp.Client.ReceiveTimeout = 5_000;

        // 1. 发送数据,触发服务端建立 UDP 会话并推送欢迎消息
        await udp.SendAsync(msgBytes, msgBytes.Length, endpoint);

        // 等待服务端完成 UDP 会话建立(轮询避免偶发时序问题)
        for (var i = 0; i < 20 && fixture.Server.SessionCount < sessionsBefore + 1; i++)
            await Task.Delay(50);

        // 首包触发 UDP 会话建立:SessionCount 精确 +1;MaxSessionCount 同步更新
        Assert.Equal(sessionsBefore + 1, fixture.Server.SessionCount);
        var expectedMax = Math.Max(maxBefore, sessionsBefore + 1);
        Assert.Equal(expectedMax, fixture.Server.MaxSessionCount);

        // 2. 收欢迎消息,精确验证整句结构(不多不少)
        //    格式:$"Welcome to visit {MachineName}!  [udp://{ip}:{port}]\r\n"
        var result1 = await udp.ReceiveAsync().WaitAsync(TimeSpan.FromSeconds(5));
        var welcome = Encoding.UTF8.GetString(result1.Buffer);
        var expectedStart = $"Welcome to visit {Environment.MachineName}!  [udp://127.0.0.1:";
        Assert.True(welcome.StartsWith(expectedStart, StringComparison.Ordinal),
            $"欢迎词应以 '{expectedStart}' 开头,实际:'{welcome}'");
        Assert.True(welcome.EndsWith("]\r\n", StringComparison.Ordinal),
            $"欢迎词应以 ']\\r\\n' 结尾,实际:'{welcome}'");
        var portPart = welcome[expectedStart.Length..welcome.IndexOf("]\r\n")];
        Assert.True(Int32.TryParse(portPart, out var clientPort) && clientPort is > 0 and <= 65535,
            $"欢迎词中端口号应为有效端口(1-65535),实际:'{portPart}'");

        // 3. 收回显,精确验证反转结果
        var result2 = await udp.ReceiveAsync().WaitAsync(TimeSpan.FromSeconds(5));
        var reply = Encoding.UTF8.GetString(result2.Buffer);
        Assert.Equal("efiLweN olleH", reply);

        // 4. 服务端队列:精确包含该消息(UDP 会话无断开语义,不检查 -1)
        await Task.Delay(50);
        Assert.Contains(msg, fixture.Server.ReceivedMessages);

        // 5. 服务端日志有接收记录
        Assert.True(fixture.ServerLog.Contains("收到"), "服务端日志应有接收记录");

        // 发送空包触发服务端断开会话
        await udp.SendAsync([], 0, endpoint);
    }

    [Fact(DisplayName = "04-ISocketClient(TCP) 全流程:连接→欢迎→发送→精确回显→服务端确认断开")]
    public async Task Test04_ISocketClient_FullFlow()
    {
        var port = fixture.Server.Port;
        var priorCount = fixture.Server.ReceivedMessages.Count;
        var sessionsBefore = fixture.Server.SessionCount;
        var maxBefore = fixture.Server.MaxSessionCount;

        var uri = new NetUri($"tcp://127.0.0.1:{port}");
        var client = uri.CreateRemote();
        client.Name = "集成测试Tcp客户";
        client.Log = XTrace.Log;

        if (client is TcpSession tcp) tcp.MaxAsync = 0;
        client.Open();  // 明确触发 TCP 连接,确保服务端在断言前建立会话

        // 等待服务端完成会话建立
        await Task.Delay(50);
        Assert.Equal(sessionsBefore + 1, fixture.Server.SessionCount);
        var expectedMax = Math.Max(maxBefore, sessionsBefore + 1);
        Assert.Equal(expectedMax, fixture.Server.MaxSessionCount);

        // 1. 接收欢迎消息,精确验证整句结构(不多不少)
        //    格式:$"Welcome to visit {MachineName}!  [tcp://{ip}:{port}]\r\n"
        using var welcome = await client.ReceiveAsync(default).WaitAsync(TimeSpan.FromSeconds(5));
        var welcomeStr = welcome.ToStr();
        var expectedStart = $"Welcome to visit {Environment.MachineName}!  [tcp://127.0.0.1:";
        Assert.True(welcomeStr.StartsWith(expectedStart, StringComparison.Ordinal),
            $"欢迎词应以 '{expectedStart}' 开头,实际:'{welcomeStr}'");
        Assert.True(welcomeStr.EndsWith("]\r\n", StringComparison.Ordinal),
            $"欢迎词应以 ']\\r\\n' 结尾,实际:'{welcomeStr}'");
        var portPart = welcomeStr[expectedStart.Length..welcomeStr.IndexOf("]\r\n")];
        Assert.True(Int32.TryParse(portPart, out var clientPort) && clientPort is > 0 and <= 65535,
            $"欢迎词中端口号应为有效端口(1-65535),实际:'{portPart}'");

        // 2. 发送消息
        const String msg = "Hello NewLife";
        client.Send(msg);

        // 3. 接收回显,精确验证反转结果
        using var reply = await client.ReceiveAsync(default).WaitAsync(TimeSpan.FromSeconds(5));
        var replyStr = reply.ToStr();
        Assert.Equal("efiLweN olleH", replyStr);

        client.Close("Test04Done");

        // 4. 服务端队列:精确多了1条(不是「大于」)
        await Task.Delay(50);
        Assert.Equal(priorCount + 1, fixture.Server.ReceivedMessages.Count);
        Assert.Contains(msg, fixture.Server.ReceivedMessages);

        // 5. 服务端日志确认断开连接
        Assert.True(await fixture.ServerLog.WaitForAsync("断开"), "服务端日志应包含断开连接记录");

        // 断开后:SessionCount 精确回到原值;MaxSessionCount 保持不减
        await Task.Delay(150);
        Assert.Equal(sessionsBefore, fixture.Server.SessionCount);
        Assert.Equal(expectedMax, fixture.Server.MaxSessionCount);
    }
}