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

namespace XUnitTest.Data;

/// <summary>数据包辅助方法的链式与零拷贝契约测试:单段快路径、跨段聚合、所有权转移与归还</summary>
public class PacketHelperChainTests
{
    private static Byte[] B(params Int32[] values)
    {
        var buf = new Byte[values.Length];
        for (var i = 0; i < values.Length; i++) buf[i] = (Byte)values[i];

        return buf;
    }

    private static IPacket Chain(String first, String second) => new ArrayPacket(first.GetBytes()).Append(second.GetBytes());

    [Fact(DisplayName = "链式文本_多字节字符被段边界切断_不产生替换字符")]
    public void ToStr_ChainedMultiByte_NoReplacement()
    {
        // 多字节字符可能正好被段边界切断,按段各自解码会把切断处的半个字符解成替换字符(U+FFFD)
        const String text = "中文abc😀测试";
        var bytes = text.GetBytes();

        // 逐个位置切一刀,全链解码结果都必须与整体解码一致
        for (var cut = 1; cut < bytes.Length; cut++)
        {
            IPacket chain = new ArrayPacket(bytes[..cut]).Append(bytes[cut..]);

            Assert.Equal(text, chain.ToStr());
        }
    }

    [Fact(DisplayName = "链式文本_带偏移与长度_同样跨段续接解码")]
    public void ToStr_ChainedMultiByte_WithOffsetAndCount()
    {
        const String text = "A中文😀B";
        const String inner = "中文😀";

        // 第二段从多字节字符中间开始:偏移跳过首字节后,解码结果应仍是完整字符串
        var bytes = text.GetBytes();
        IPacket chain = new ArrayPacket(bytes[..2]).Append(bytes[2..]);

        Assert.Equal(inner, chain.ToStr(null, 1, inner.GetBytes().Length));
    }

    #region TryGetSpan
    [Fact]
    [DisplayName("获取跨度_单段成功_链式失败")]
    public void TryGetSpan_SingleVsChain()
    {
        IPacket single = new ArrayPacket(B(1, 2, 3));
        Assert.True(single.TryGetSpan(out var span));
        Assert.Equal(B(1, 2, 3), span.ToArray());

        // 链式无法用单块跨度表示(会遗漏后续节点),必须返回 false 让调用方走拼读路径
        IPacket chain = Chain("\u0001", "\u0002");
        Assert.False(chain.TryGetSpan(out var none));
        Assert.True(none.IsEmpty);
    }
    #endregion

    #region GetPrefix
    [Fact]
    [DisplayName("前缀拼读_首段足够_直接返回首段跨度")]
    public void GetPrefix_FirstSegmentEnough()
    {
        IPacket pk = new ArrayPacket(B(1, 2, 3, 4, 5));
        Span<Byte> buffer = stackalloc Byte[8];

        var prefix = pk.GetPrefix(buffer, 4);

        // 首段已足够:零拷贝直引首段跨度,不裁剪也不拷贝(可能长于 count,调用方按需取前 count 字节)
        Assert.True(prefix.Length >= 4);
        Assert.Equal(B(1, 2, 3, 4), prefix[..4].ToArray());
    }

    [Fact]
    [DisplayName("前缀拼读_跨段_拼入缓冲")]
    public void GetPrefix_CrossSegment()
    {
        IPacket pk = Chain("\u0001\u0002", "\u0003\u0004\u0005");
        Span<Byte> buffer = stackalloc Byte[4];

        var prefix = pk.GetPrefix(buffer, 4);

        Assert.Equal(B(1, 2, 3, 4), prefix.ToArray());
    }

    [Fact]
    [DisplayName("前缀拼读_数据不足_返回短跨度由解析方判定")]
    public void GetPrefix_NotEnoughData()
    {
        IPacket pk = new ArrayPacket(B(1, 2));
        Span<Byte> buffer = stackalloc Byte[4];

        var prefix = pk.GetPrefix(buffer, 4);

        Assert.Equal(B(1, 2), prefix.ToArray());
    }
    #endregion

    #region ToSegment
    [Fact]
    [DisplayName("转数组段_单包零拷贝_直接引用底层数组")]
    public void ToSegment_Single_ZeroCopy()
    {
        var buf = "Stone".GetBytes();
        IPacket pk = new ArrayPacket(buf);

        var seg = pk.ToSegment();

        Assert.Same(buf, seg.Array);
        Assert.Equal(0, seg.Offset);
        Assert.Equal(5, seg.Count);
    }

    [Fact]
    [DisplayName("转数组段_链式_聚合为单段副本")]
    public void ToSegment_Chain_Aggregates()
    {
        IPacket pk = Chain("Stone", "NewLife");

        var seg = pk.ToSegment();

        Assert.Equal(12, seg.Count);
        Assert.Equal("StoneNewLife", seg.Array!.ToStr(null, seg.Offset, seg.Count));
    }

    [Fact]
    [DisplayName("转数组段_窗口偏移_保持窗口与长度")]
    public void ToSegment_Window()
    {
        var buf = "StoneNewLife".GetBytes();
        IPacket pk = new ArrayPacket(buf, 5, 7);

        var seg = pk.ToSegment();

        Assert.Same(buf, seg.Array);
        Assert.Equal(5, seg.Offset);
        Assert.Equal(7, seg.Count);
    }
    #endregion

    #region ReadBytes
    [Fact]
    [DisplayName("读字节_单段整段_零拷贝返回底层数组")]
    public void ReadBytes_FullSingle_ReturnsUnderlyingArray()
    {
        var buf = "Stone".GetBytes();
        IPacket pk = new ArrayPacket(buf);

        // 读取全部且数组段完整时直接返回底层数组(调用方需知晓这是别名而非副本)
        Assert.Same(buf, pk.ReadBytes());

        // 带窗口的包不满足“数组段完整”条件,必须复制
        var window = new ArrayPacket(buf, 1, 3);
        Assert.NotSame(buf, window.ReadBytes());
        Assert.Equal("ton", window.ReadBytes().ToStr());
    }

    [Fact]
    [DisplayName("读字节_链式跨段_按全局偏移取值")]
    public void ReadBytes_Chain_CrossSegment()
    {
        IPacket pk = Chain("Stone", "NewLife");

        Assert.Equal("StoneNewLife", pk.ReadBytes().ToStr());
        Assert.Equal("eNe", pk.ReadBytes(4, 3).ToStr());
        Assert.Equal("NewLife", pk.ReadBytes(5).ToStr());

        // 超出范围夹紧到可用长度,不做越窗读取
        Assert.Equal("NewLife", pk.ReadBytes(5, 999).ToStr());
        Assert.Empty(pk.ReadBytes(12));
    }
    #endregion

    #region Clone
    [Fact]
    [DisplayName("克隆_单包_独立副本且改副本不影响源")]
    public void Clone_Single_Independent()
    {
        var buf = "Stone".GetBytes();
        IPacket src = new ArrayPacket(buf);

        using var clone = (IOwnerPacket)src.Clone();

        Assert.Equal(src.Total, clone.Total);
        Assert.Equal("Stone", clone.ToStr());

        // 深拷贝:底层缓冲不共享
        clone.GetSpan()[0] = (Byte)'X';
        Assert.Equal("Stone", src.ToStr());
        Assert.Equal("Xtone", clone.ToStr());
    }

    [Fact]
    [DisplayName("克隆_链式_聚合为单段拥有句柄")]
    public void Clone_Chain_AggregatesToSingleSegment()
    {
        IPacket src = Chain("Stone", "NewLife");

        using var clone = (IOwnerPacket)src.Clone();

        Assert.Equal(src.Total, clone.Total);
        Assert.Equal("StoneNewLife", clone.ToStr());

        // 克隆结果自带独立缓冲,不沿用源链
        Assert.Null(clone.Next);
        Assert.NotSame(((ArrayPacket)src).Buffer, ((OwnerPacket)clone).Buffer);
    }

    [Fact]
    [DisplayName("克隆_产出的拥有句柄_释放后归还引用")]
    public void Clone_Dispose_ReleasesReference()
    {
        IPacket src = new ArrayPacket("Stone".GetBytes());

        var clone = (OwnerPacket)src.Clone();
        Assert.Equal(1, clone.RefCount);

        clone.Dispose();
        Assert.Equal(0, clone.RefCount);
    }
    #endregion

    #region WriteTo
    [Fact]
    [DisplayName("写文本_单包_按编码解码写入")]
    public void WriteTo_SinglePacket()
    {
        IPacket pk = new ArrayPacket("StoneNewLife".GetBytes());
        var writer = new StringWriter();

        pk.WriteTo(writer);

        Assert.Equal("StoneNewLife", writer.ToString());
    }

    [Fact]
    [DisplayName("写文本_链式_逐段顺序拼接")]
    public void WriteTo_Chain()
    {
        IPacket pk = Chain("Stone", "NewLife");
        var writer = new StringWriter();

        pk.WriteTo(writer);

        Assert.Equal("StoneNewLife", writer.ToString());
    }

    [Fact]
    [DisplayName("写文本_指定编码与空包_安全处理")]
    public void WriteTo_EncodingAndEmpty()
    {
        IPacket pk = new ArrayPacket(Encoding.Unicode.GetBytes("新生命"));
        var writer = new StringWriter();
        pk.WriteTo(writer, Encoding.Unicode);
        Assert.Equal("新生命", writer.ToString());

        // 空包与空写入器调用安全,不抛异常
        var empty = new StringWriter();
        new ArrayPacket(Array.Empty<Byte>()).WriteTo(empty);
        Assert.Equal(String.Empty, empty.ToString());

        PacketHelper.WriteTo(null!, empty);
        Assert.Equal(String.Empty, empty.ToString());
    }

    [Fact]
    [DisplayName("写文本_跨段切断多字节字符_不产生替换字符")]
    public void WriteTo_Chain_SplitMultiByteChar()
    {
        // “新生命”UTF-8 每字 3 字节,在第 2 字节处切断:半个字符落在第一段末尾
        var bytes = "新生命".GetBytes();
        IPacket pk = new ArrayPacket(bytes[..2]).Append(bytes[2..]);

        var writer = new StringWriter();
        pk.WriteTo(writer);

        // 逐段解码必须经解码器保持状态,否则切断处会解成替换字符
        Assert.Equal("新生命", writer.ToString());

        // 对照:逐段各自解码(无状态)确实会解错——说明本断言对“是否保持解码状态”有辨别力
        var perSegment = Encoding.UTF8.GetString(bytes[..2]) + Encoding.UTF8.GetString(bytes[2..]);
        Assert.NotEqual("新生命", perSegment);
    }

    [Fact]
    [DisplayName("写文本_截断序列_按编码规则产生替换字符")]
    public void WriteTo_TruncatedSequence_Replacement()
    {
        // 只有半个字符(序列本身不完整):收尾时按编码规则解成替换字符,不抛异常
        var bytes = "新".GetBytes()[..2];

        var writer = new StringWriter();
        new ArrayPacket(bytes).WriteTo(writer);

        Assert.Equal("\uFFFD", writer.ToString());
    }
    #endregion
}