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

namespace XUnitTest.Net;

/// <summary>Unix域套接字传输测试。依赖操作系统的AF_UNIX支持(Linux/macOS,Windows 10 1803+),不支持时静默跳过</summary>
[Collection("Net.D")]
[DisplayName("Unix域套接字测试")]
public class UdsNetServerTests
{
    #region 辅助
    private static String NewTempPath() => Path.Combine(Path.GetTempPath(), $"nl_uds_{Guid.NewGuid():N}.sock");

    /// <summary>探测当前系统是否支持Unix域套接字</summary>
    private static Boolean UnixSupported()
    {
        var path = NewTempPath();
        try
        {
            using var sock = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            sock.Bind(new UnixDomainSocketEndPoint(path));

            return true;
        }
        catch
        {
            return false;
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }
    #endregion

    [Fact]
    [DisplayName("Unix域套接字_Echo回环")]
    public void Echo()
    {
        if (!UnixSupported())
        {
            XTrace.WriteLine("当前系统不支持Unix域套接字,跳过测试");
            return;
        }

        var path = NewTempPath();
        try
        {
            using var server = new UdsEchoServer { Local = new NetUri($"unix://{path}") };
            server.Start();

            Assert.True(server.Active);
            Assert.True(File.Exists(path));

            using var client = new NetUri($"unix://{path}").CreateRemote();

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

            client.Open();

            var payload = new Byte[32];
            Random.Shared.NextBytes(payload);
            _ = client.Send(payload);

            Assert.True(wait.Wait(3_000));
            Assert.NotNull(received);
            Assert.Equal(payload, received);
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_编解码器Echo")]
    public async Task CodecEcho()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        try
        {
            using var server = new NetServer
            {
                Local = new NetUri($"unix://{path}"),
                Protocol = new SrmpCodec(),
            };
            server.Received += async (s, e) =>
            {
                if (s is not INetSession session) return;
                if (e.Message is not DefaultMessage m) return;

                // 回显解码后的消息体
                var reply = m.CreateReply();
                if (m.Body != null) reply.SetBody(await m.Body.ReadAllAsync());
                session.SendReply(reply, e);
            };
            server.Start();

            Assert.True(server.Active);

            using var client = new NetUri($"unix://{path}").CreateRemote();
            ((SessionBase)client).Protocol = new SrmpCodec();
            client.Open();

            var request = new DefaultMessage();
            request.SetBody(new ArrayPacket("Hello UDS"u8.ToArray()));
            var response = await client.SendMessageAsync(request);

            Assert.NotNull(response);
            var respMsg = Assert.IsAssignableFrom<IMessage>(response);
            var body = await respMsg.Body!.ReadAllAsync();
            Assert.Equal("Hello UDS"u8.ToArray(), body.AsReadOnlySequence().ToArray());
            body.TryDispose();
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_停止后删除套接字文件")]
    public void StopDeletesFile()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        var server = new NetServer { Local = new NetUri($"unix://{path}") };
        server.Start();

        Assert.True(File.Exists(path));

        server.Stop("Test");
        server.Dispose();

        Assert.False(File.Exists(path));
    }

    [Fact]
    [DisplayName("Unix域套接字_自动清理残留文件后重启")]
    public void CleanResidualFile()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        try
        {
            // 制造残留:绑定原始套接字后关闭,不删除文件(模拟进程异常退出)
            using (var raw = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified))
            {
                raw.Bind(new UnixDomainSocketEndPoint(path));
            }

            // 平台自动删除文件时,本用例退化为普通启动场景
            using var server = new NetServer { Local = new NetUri($"unix://{path}") };
            server.Start();

            Assert.True(server.Active);
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_已被监听时拒绝启动")]
    public void RejectWhenInUse()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        try
        {
            using var server1 = new NetServer { Local = new NetUri($"unix://{path}") };
            server1.Start();
            Assert.True(server1.Active);

            // 第二个实例使用相同路径时,应拒绝启动而不是抢占
            using var server2 = new NetServer { Local = new NetUri($"unix://{path}") };
            var ex = Assert.Throws<InvalidOperationException>(() => server2.Start());
            Assert.Contains(path, ex.Message);
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_HttpServer冒烟")]
    public void HttpOverUds()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        try
        {
            using var server = new HttpServer { Local = new NetUri($"unix://{path}") };
            server.MapGet("/ping", () => "pong");
            server.Start();

            Assert.True(server.Active);

            using var client = new NetUri($"unix://{path}").CreateRemote();

            // HTTP响应可能分片到达,用事件收集;同步Receive会与后台接收环竞争同一连接
            var wait = new ManualResetEventSlim();
            var text = "";
            client.Received += (s, e) =>
            {
                var bytes = e.GetBytes();
                if (bytes != null) text += bytes.ToStr();

                if (text.Contains("pong")) wait.Set();
            };

            client.Open();

            var request = "GET /ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".GetBytes();
            _ = client.Send(request);

            Assert.True(wait.Wait(3_000), "未收到HTTP响应");

            Assert.Contains("200", text);
            Assert.Contains("pong", text);
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_静态文件响应不因内核零拷贝挂起")]
    public void StaticFileOverUds()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        var root = Path.Combine(Path.GetTempPath(), "nl_uds_www_" + Guid.NewGuid().ToString("N")[..8]);
        Directory.CreateDirectory(root);
        File.WriteAllText(Path.Combine(root, "hello.txt"), "hello-uds");
        try
        {
            using var server = new HttpServer { Local = new NetUri($"unix://{path}") };
            server.MapStaticFiles("/www", root);
            server.Start();

            Assert.True(server.Active);

            using var client = new NetUri($"unix://{path}").CreateRemote();

            var wait = new ManualResetEventSlim();
            var text = "";
            client.Received += (s, e) =>
            {
                var bytes = e.GetBytes();
                if (bytes != null) text += bytes.ToStr();

                if (text.Contains("hello-uds")) wait.Set();
            };

            client.Open();

            var request = "GET /www/hello.txt HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".GetBytes();
            _ = client.Send(request);

            // 旧实现:UDS 上内核零拷贝既不会推进也不会报错,响应永不返回(此处超时);
            // 修复后判定套接字类型走分块发送,数秒内返回文件内容
            Assert.True(wait.Wait(5_000), "静态文件响应超时:UDS 上内核零拷贝未降级");
            Assert.Contains("200", text);
            Assert.Contains("hello-uds", text);
        }
        finally
        {
            try { File.Delete(path); } catch { }
            try { Directory.Delete(root, true); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_对端不读时发送文件不挂起")]
    public async Task SendFileOnUds_NonReadingPeer_Completes()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        var file = Path.Combine(Path.GetTempPath(), $"nl_uds_file_{Guid.NewGuid():N}.bin");
        File.WriteAllBytes(file, new Byte[1024]);
        try
        {
            using var listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            listener.Bind(new UnixDomainSocketEndPoint(path));
            listener.Listen(1);

            using var client = (TcpSession)new NetUri($"unix://{path}").CreateRemote();
            client.Open();

            // 对端接受连接但永不读取:UDS 上内核零拷贝(sendfile/TransmitFile)会永不完成(TCP 同条件 0ms 返回),
            // 旧实现会在此挂死(无取消令牌时无限期占用发送线程)。修复后判定套接字类型走分块发送,1KB 直接入缓冲区完成
            using var accepted = await listener.AcceptWithinAsync();

            using var cts = new CancellationTokenSource(5_000);
            Assert.Equal(1024, await client.SendFileAsync(file, cts.Token));
        }
        finally
        {
            try { File.Delete(path); } catch { }
            try { File.Delete(file); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_拉取模式收发")]
    public async Task PullModeEcho()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        try
        {
            using var server = new NetServer { Local = new NetUri($"unix://{path}") };
            server.Received += (s, e) =>
            {
                if (s is INetSession session && e.Packet != null) session.Send(e.Packet);
            };
            server.Start();

            // 拉取模式客户端:打开前关闭自动接收环
            using var client = new TcpSession
            {
                Remote = new NetUri($"unix://{path}"),
                AutoReceive = false,
                Log = XTrace.Log,
            };
            client.Open();

            _ = client.Send("uds-pull");

            using var pk = await client.ReceiveAsync(default).WaitAsync(TimeSpan.FromSeconds(5));
            Assert.NotNull(pk);
            Assert.Equal("uds-pull", pk!.ToStr());
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    [Fact]
    [DisplayName("Unix域套接字_拉取时对端关闭_不挂死")]
    public async Task PullModeRemoteClose()
    {
        if (!UnixSupported()) return;

        var path = NewTempPath();
        try
        {
            // 服务端:收到数据后主动关闭会话
            using var server = new NetServer { Local = new NetUri($"unix://{path}") };
            server.Received += (s, e) =>
            {
                if (s is NetSession ns) ns.Close("server-close");
            };
            server.Start();

            using var client = new TcpSession
            {
                Remote = new NetUri($"unix://{path}"),
                AutoReceive = false,
                Log = XTrace.Log,
            };
            client.Open();

            _ = client.Send("bye");

            // 对端关闭后应结束拉取(空引用/空包或连接级异常),而不是永久挂起
            try
            {
                using var pk = await client.ReceiveAsync(default).WaitAsync(TimeSpan.FromSeconds(5));
                Assert.True(pk == null || pk.Length == 0, $"对端关闭后拉取应结束,实际收到 {pk?.Length} 字节");
            }
            catch (Exception ex) when (ex is IOException or SocketException or ObjectDisposedException)
            {
                // 连接级异常亦为有效断开语义
            }
        }
        finally
        {
            try { File.Delete(path); } catch { }
        }
    }

    #region 服务端
    class UdsEchoServer : NetServer<UdsEchoSession>
    {
    }

    class UdsEchoSession : NetSession<UdsEchoServer>
    {
        protected override void OnReceive(ReceivedEventArgs e)
        {
            var packet = e.Packet;
            if (packet == null || packet.Length == 0) return;

            Send(packet);
        }
    }
    #endregion
}