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

namespace XUnitTest.Net;

/// <summary>NetClient 应用层网络客户端单元测试</summary>
[Collection("Net")]
public class NetClientTests
{
    #region 构造与属性

    [Fact]
    public void DefaultCtor_NameIsClassName()
    {
        using var client = new NetClient();
        Assert.Equal("NetClient", client.Name);
        Assert.Null(client.Remote);
        Assert.Null(client.Server);
        Assert.False(client.Active);
        Assert.Null(client.Client);
    }

    [Fact]
    public void StringCtor_ServerParsedToRemote()
    {
        using var client = new NetClient("tcp://127.0.0.1:8080");
        Assert.NotNull(client.Remote);
        Assert.Equal(NetType.Tcp, client.Remote!.Type);
        // IP 地址解析后存入 Address,域名才会写入 Host
        Assert.Equal("127.0.0.1", client.Remote.Address?.ToString());
        Assert.Equal(8080, client.Remote.Port);
    }

    [Fact]
    public void NetUriCtor_RemoteAssigned()
    {
        var uri = new NetUri("udp://127.0.0.1:9090");
        using var client = new NetClient(uri);
        Assert.Same(uri, client.Remote);
    }

    [Fact]
    public void Server_SetNull_ClearsRemote()
    {
        using var client = new NetClient("tcp://127.0.0.1:8080");
        Assert.NotNull(client.Remote);
        client.Server = null;
        Assert.Null(client.Remote);
        Assert.Null(client.Server);
    }

    [Fact]
    public void Server_SetEmpty_ClearsRemote()
    {
        using var client = new NetClient("tcp://127.0.0.1:8080");
        client.Server = "";
        Assert.Null(client.Remote);
    }

    [Fact]
    public void ToString_ReturnsRemoteOrName()
    {
        using var c1 = new NetClient();
        Assert.Equal("NetClient", c1.ToString());

        using var c2 = new NetClient("tcp://127.0.0.1:1234");
        Assert.Contains("1234", c2.ToString());
    }

    [Fact]
    public void DefaultPropertyValues()
    {
        using var client = new NetClient();
        Assert.Equal(3_000, client.Timeout);
        Assert.True(client.AutoReconnect);
        Assert.Equal(5_000, client.ReconnectDelay);
        Assert.Equal(0, client.MaxReconnect);
        Assert.Null(client.Pipeline);
        Assert.Null(client.Tracer);
    }

    #endregion

    #region 扩展数据

    [Fact]
    public void Items_LazyCreated()
    {
        using var client = new NetClient();
        var items = client.Items;
        Assert.NotNull(items);
        Assert.Same(items, client.Items);
    }

    [Fact]
    public void Indexer_GetSet()
    {
        using var client = new NetClient();
        Assert.Null(client["key1"]);
        client["key1"] = "value1";
        Assert.Equal("value1", client["key1"]);
        client["key1"] = null;
        Assert.Null(client["key1"]);
    }

    #endregion

    #region 日志

    [Fact]
    public void LogPrefix_DefaultFromName()
    {
        using var client = new NetClient();
        Assert.Equal("NetClient ", client.LogPrefix);
    }

    [Fact]
    public void LogPrefix_CustomSet()
    {
        using var client = new NetClient();
        client.LogPrefix = "[MyApp] ";
        Assert.Equal("[MyApp] ", client.LogPrefix);
    }

    [Fact]
    public void WriteLog_RoutesToLog()
    {
        var captured = new List<String>();
        var log = new ActionLog(msg => captured.Add(msg));

        using var client = new NetClient { Log = log };
        client.WriteLog("test {0}", 42);

        Assert.Single(captured);
        Assert.Contains("test 42", captured[0]);
    }

    #endregion

    #region 编解码器

    [Fact]
    public void Add_Instance_CreatesPipeline()
    {
        using var client = new NetClient();
        Assert.Null(client.Pipeline);
        var result = client.Add(new StandardCodec());
        Assert.NotNull(client.Pipeline);
        Assert.Same(client, result);
    }

    [Fact]
    public void Add_Generic_CreatesPipeline()
    {
        using var client = new NetClient();
        var result = client.Add<StandardCodec>();
        Assert.NotNull(client.Pipeline);
        Assert.Same(client, result);
    }

    [Fact]
    public void Add_Multiple_HandlersOrdered()
    {
        using var client = new NetClient();
        client.Add<StandardCodec>().Add<StandardCodec>();
        Assert.NotNull(client.Pipeline);
        Assert.Equal(2, ((Pipeline)client.Pipeline!).Handlers.Count);
    }

    #endregion

    #region 连接管理(不依赖真实服务端)

    [Fact]
    public void Open_NoRemote_Throws()
    {
        using var client = new NetClient();
        Assert.Throws<InvalidOperationException>(() => client.Open());
    }

    [Fact]
    public async Task OpenAsync_NoRemote_Throws()
    {
        using var client = new NetClient();
        await Assert.ThrowsAsync<InvalidOperationException>(() => client.OpenAsync());
    }

    [Fact]
    public void Open_AfterDispose_ReturnsFalse()
    {
        var client = new NetClient("tcp://127.0.0.1:1");
        client.Dispose();
        Assert.False(client.Open());
    }

    [Fact]
    public void Close_WhenNotConnected_ReturnsTrueIdempotent()
    {
        using var client = new NetClient("tcp://127.0.0.1:1");
        Assert.True(client.Close("test"));
    }

    [Fact]
    public async Task CloseAsync_WhenNotConnected_ReturnsTrueIdempotent()
    {
        using var client = new NetClient("tcp://127.0.0.1:1");
        Assert.True(await client.CloseAsync("test"));
    }

    [Fact]
    public void Open_UnreachableHost_ReturnsFalse()
    {
        using var client = new NetClient("tcp://127.0.0.1:1") { Timeout = 500, AutoReconnect = false };
        Assert.False(client.Open());
        Assert.False(client.Active);
        Assert.Null(client.Client);
    }

    [Fact]
    public void Send_NotConnected_Throws()
    {
        using var client = new NetClient("tcp://127.0.0.1:1") { Timeout = 200, AutoReconnect = false };
        Assert.Throws<InvalidOperationException>(() => client.Send(new Byte[] { 1, 2, 3 }));
    }

    #endregion

    #region 真实 TCP 连接(Echo 服务端)

    [Fact]
    public void TcpOpenClose()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        Assert.True(client.Open());
        Assert.True(client.Active);
        Assert.NotNull(client.Client);

        Assert.True(client.Close("done"));
        Assert.False(client.Active);
    }

    [Fact]
    public void Open_WhenAlreadyActive_ReturnsTrueNoRebuild()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        client.Open();
        var firstInner = client.Client;

        Assert.True(client.Open());
        Assert.Same(firstInner, client.Client);
    }

    [Fact]
    public async Task OpenAsync_Success()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        Assert.True(await client.OpenAsync());
        Assert.True(client.Active);
    }

    [Fact]
    public async Task CloseAsync_Success()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        client.Open();
        Assert.True(await client.CloseAsync("done"));
        Assert.False(client.Active);
    }

    [Fact]
    public void Dispose_CleansUp()
    {
        using var server = CreateEchoServer();
        var client = CreateTcpClient(server.Port);
        client.Open();
        client.Dispose();

        Assert.False(client.Active);
        Assert.Null(client.Client);
        Assert.True(client.Disposed);
    }

    [Fact]
    public void SendReceive_ByteArray_Echo()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var wait = new ManualResetEventSlim();
        Byte[]? received = null;
        client.Received += (s, e) => { received = e.GetBytes(); wait.Set(); };

        client.Open();

        var payload = new Byte[] { 0x01, 0x02, 0x03, 0x04 };
        client.Send(payload);

        Assert.True(wait.Wait(3_000), "超时未收到回声");
        Assert.Equal(payload, received);
    }

    [Fact]
    public void Send_ArraySegment_Echo()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var wait = new ManualResetEventSlim();
        Byte[]? received = null;
        client.Received += (s, e) => { received = e.GetBytes(); wait.Set(); };

        client.Open();

        var buf = new Byte[] { 0xAA, 0x01, 0x02, 0x03, 0xBB };
        client.Send(new ArraySegment<Byte>(buf, 1, 3));

        Assert.True(wait.Wait(3_000), "超时未收到回声");
        Assert.Equal(new Byte[] { 0x01, 0x02, 0x03 }, received);
    }

    [Fact]
    public void Send_ReadOnlySpan_Echo()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var wait = new ManualResetEventSlim();
        Byte[]? received = null;
        client.Received += (s, e) => { received = e.GetBytes(); wait.Set(); };

        client.Open();

        ReadOnlySpan<Byte> span = new Byte[] { 0x10, 0x20, 0x30 };
        client.Send(span);

        Assert.True(wait.Wait(3_000), "超时未收到回声");
        Assert.Equal(new Byte[] { 0x10, 0x20, 0x30 }, received);
    }

    [Fact]
    public async Task ReceiveAsync_EchoData()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var tcs = new TaskCompletionSource<Byte[]?>();
        client.Received += (s, e) => tcs.TrySetResult(e.GetBytes());

        client.Open();

        var payload = new Byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
        client.Send(payload);

        using var cts = new CancellationTokenSource(3_000);
        cts.Token.Register(() => tcs.TrySetCanceled());
        var result = await tcs.Task;
        Assert.Equal(payload, result);
    }

    [Fact]
    public void Received_EventDriven_Echo()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var wait = new ManualResetEventSlim();
        Byte[]? received = null;

        client.Received += (s, e) =>
        {
            received = e.GetBytes();
            wait.Set();
        };

        client.Open();
        client.Send(new Byte[] { 1, 2, 3, 4, 5 });

        Assert.True(wait.Wait(3_000), "Received 事件超时未触发");
        Assert.NotNull(received);
        Assert.Equal(new Byte[] { 1, 2, 3, 4, 5 }, received);
    }

    [Fact]
    public void Received_Sender_IsNetClient()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        Object? capturedSender = null;
        var wait = new ManualResetEventSlim();

        client.Received += (s, e) => { capturedSender = s; wait.Set(); };

        client.Open();
        client.Send(new Byte[] { 0xFF });

        wait.Wait(3_000);
        Assert.Same(client, capturedSender);
    }

    [Fact]
    public void Opened_Event_FiredOnConnect()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        Object? capturedSender = null;
        var wait = new ManualResetEventSlim();

        client.Opened += (s, e) => { capturedSender = s; wait.Set(); };
        client.Open();

        Assert.True(wait.Wait(2_000), "Opened 事件未触发");
        Assert.Same(client, capturedSender);
    }

    [Fact]
    public void Closed_Event_FiredOnClose()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var wait = new ManualResetEventSlim();
        Object? capturedSender = null;

        client.Closed += (s, e) => { capturedSender = s; wait.Set(); };
        client.Open();
        client.Close("test");

        Assert.True(wait.Wait(2_000), "Closed 事件未触发");
        Assert.Same(client, capturedSender);
    }

    [Fact]
    public void Pipeline_PassedToInnerClient()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);
        client.Add<StandardCodec>();

        client.Open();

        Assert.NotNull(client.Pipeline);
        Assert.Same(client.Pipeline, client.Client!.Pipeline);
    }

    [Fact]
    public void Tracer_PassedToInnerClient()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);

        var tracer = new DefaultTracer();
        client.Tracer = tracer;
        client.Open();

        Assert.Same(tracer, client.Client!.Tracer);
    }

    [Fact]
    public void Name_And_Timeout_PassedToInnerClient()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);
        client.Name = "MyTestClient";
        client.Timeout = 2_000;

        client.Open();

        Assert.Equal("MyTestClient", client.Client!.Name);
        Assert.Equal(2_000, client.Client.Timeout);
    }

    #endregion

    #region 断线重连

    [Fact]
    public void Close_NoAutoReconnect_AfterUserClose()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);
        client.AutoReconnect = true;
        client.ReconnectDelay = 200;

        client.Open();

        var openCount = 0;
        var reconnected = false;
        client.Opened += (s, e) =>
        {
            if (Interlocked.Increment(ref openCount) > 1) reconnected = true;
        };

        client.Close("user close");
        Thread.Sleep(500);

        Assert.False(reconnected, "主动关闭后不应触发重连");
    }

    [Fact]
    public void AutoReconnect_Disabled_NoReconnect()
    {
        using var server = CreateEchoServer();
        using var client = CreateTcpClient(server.Port);
        client.AutoReconnect = false;
        client.ReconnectDelay = 200;

        client.Open();

        var openCount = 0;
        var reconnectCount = 0;
        client.Opened += (s, e) =>
        {
            if (Interlocked.Increment(ref openCount) > 1) Interlocked.Increment(ref reconnectCount);
        };

        server.Stop("test");
        Thread.Sleep(500);

        Assert.Equal(0, reconnectCount);
    }

    [Fact]
    public void MaxReconnect_StopsAfterLimit()
    {
        using var client = new FakeReconnectClient("tcp://127.0.0.1:1")
        {
            AutoReconnect = true,
            MaxReconnect = 2,
        };
        client.SimulateReconnect(2);
        Assert.False(client.CanScheduleReconnect(), "超过最大重连次数后不应再调度");
    }

    [Fact]
    public void MaxReconnect_Zero_AlwaysAllowed()
    {
        using var client = new FakeReconnectClient("tcp://127.0.0.1:1")
        {
            AutoReconnect = true,
            MaxReconnect = 0,
        };
        client.SimulateReconnect(100);
        Assert.True(client.CanScheduleReconnect(), "MaxReconnect=0 时始终允许重连");
    }

    [Fact]
    public void Dispose_PreventsReconnect()
    {
        using var server = CreateEchoServer();
        var client = CreateTcpClient(server.Port);
        client.AutoReconnect = true;
        client.ReconnectDelay = 100;

        client.Open();

        var openCount = 0;
        var reconnected = false;
        client.Opened += (s, e) =>
        {
            if (Interlocked.Increment(ref openCount) > 1) reconnected = true;
        };

        client.Dispose();
        Thread.Sleep(300);

        Assert.False(reconnected, "Dispose 后不应触发重连");
    }

    [Fact]
    public void AutoReconnect_ReconnectsAfterServerRestart()
    {
        var server = CreateEchoServer();
        var port = server.Port;

        using var client = new NetClient($"tcp://127.0.0.1:{port}")
        {
            Timeout = 3_000,
            AutoReconnect = true,
            ReconnectDelay = 300,
        };

        client.Open();
        Assert.True(client.Active);

        var openCount = 0;
        var reconnectWait = new ManualResetEventSlim();
        client.Opened += (s, e) =>
        {
            // 注册 handler 在首次 Open 之后,所以任意触发即为重连
            if (Interlocked.Increment(ref openCount) > 0) reconnectWait.Set();
        };

        server.Stop("restart test");
        server.Dispose();
        Thread.Sleep(300);

        using var server2 = new NetServer
        {
            Port = port,
            ProtocolType = NetType.Tcp,
            AddressFamily = AddressFamily.InterNetwork,
            ReuseAddress = true,
        };
        server2.Start();

        var ok = reconnectWait.Wait(5_000);
        server2.Stop("done");

        Assert.True(ok, "未能在超时时间内成功重连");
    }

    #endregion

    #region CreateClient 扩展点

    [Fact]
    public void CreateClient_Overridable()
    {
        using var server = CreateEchoServer();
        using var custom = new CustomNetClient(server.Port);

        custom.Open();

        Assert.True(custom.CustomCreateClientCalled);
        Assert.True(custom.Active);
    }

    #endregion

    #region 辅助类型

    private static NetServer CreateEchoServer()
    {
        var server = new NetServer<EchoSession>
        {
            Port = 0,
            ProtocolType = NetType.Tcp,
            AddressFamily = AddressFamily.InterNetwork,
            UseSession = false,
        };
        server.Start();
        return server;
    }

    private static NetClient CreateTcpClient(Int32 port) =>
        new($"tcp://127.0.0.1:{port}") { Timeout = 3_000, AutoReconnect = false };

    private class EchoSession : NetSession<NetServer<EchoSession>>
    {
        protected override void OnReceive(ReceivedEventArgs e)
        {
            var pk = e.Packet;
            if (pk != null && pk.Length > 0) Send(pk);
        }
    }

    private class ActionLog : Logger
    {
        private readonly Action<String> _action;

        public ActionLog(Action<String> action) => _action = action;

        protected override void OnWrite(LogLevel level, String format, params Object?[] args)
        {
            var msg = args == null || args.Length == 0 ? format : String.Format(format, args);
            _action(msg);
        }
    }

    private class FakeReconnectClient : NetClient
    {
        private Int32 _count;

        public FakeReconnectClient(String server) : base(server) { }

        public void SimulateReconnect(Int32 count) => _count = count;

        public Boolean CanScheduleReconnect()
        {
            if (!AutoReconnect || Disposed) return false;
            if (MaxReconnect > 0 && _count >= MaxReconnect) return false;
            return true;
        }
    }

    private class CustomNetClient : NetClient
    {
        public Boolean CustomCreateClientCalled { get; private set; }

        public CustomNetClient(Int32 port) : base($"tcp://127.0.0.1:{port}") { }

        protected override ISocketClient CreateClient()
        {
            CustomCreateClientCalled = true;
            return base.CreateClient();
        }
    }

    #endregion
}