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

namespace XUnitTest.Data;

/// <summary>OwnerPacket 专门单元测试</summary>
public class OwnerPacketTests
{
    #region 构造函数测试

    [Fact(DisplayName = "构造函数:创建指定长度的内存包")]
    public void Constructor_WithLength_ShouldCreatePacketCorrectly()
    {
        using var packet = new OwnerPacket(100);

        Assert.NotNull(packet.Buffer);
        Assert.True(packet.Buffer.Length >= 100);
        Assert.Equal(0, packet.Offset);
        Assert.Equal(100, packet.Length);
        Assert.Equal(100, packet.Total);
        Assert.Null(packet.Next);
        Assert.NotNull(packet.GetValue("_owner"));
    }

    [Fact(DisplayName = "构造函数:零长度包")]
    public void Constructor_WithZeroLength_ShouldCreateEmptyPacket()
    {
        using var packet = new OwnerPacket(0);

        Assert.NotNull(packet.Buffer);
        Assert.Equal(0, packet.Length);
        Assert.Equal(0, packet.Total);
    }

    [Fact(DisplayName = "构造函数:使用现有缓冲区")]
    public void Constructor_WithExistingBuffer_ShouldCreatePacketCorrectly()
    {
        var buffer = new Byte[200];
        // 注意:使用 hasOwner = false 避免 ArrayPool 返回错误
        var packet = new OwnerPacket(buffer, 10, 50, false);
        try
        {
            Assert.Same(buffer, packet.Buffer);
            Assert.Equal(10, packet.Offset);
            Assert.Equal(50, packet.Length);
            Assert.Equal(50, packet.Total);
            Assert.Null(packet.GetValue("_owner"));
        }
        finally
        {
            packet.Dispose(); // 借用视图 Dispose 无操作,不会尝试返回到 ArrayPool
        }
    }

    [Fact(DisplayName = "构造函数:头部扩展构造")]
    public void Constructor_WithHeaderExpansion_ShouldTransferOwnership()
    {
        // 创建一个从偏移位置开始的包,这样就有前置空间可以扩展
        var buffer = new Byte[200];
        var originalPacket = new OwnerPacket(buffer, 30, 50, false); // offset=30,这样前面有30字节可以扩展
        originalPacket.GetSpan().Fill(0x42);
        var expandSize = 20;

        try
        {
            var expandedPacket = new OwnerPacket(originalPacket, expandSize);

            try
            {
                Assert.Same(buffer, expandedPacket.Buffer);
                Assert.Equal(30 - expandSize, expandedPacket.Offset);
                Assert.Equal(50 + expandSize, expandedPacket.Length);

                // 验证所有权移动:源实例整体作废
                Assert.Null(originalPacket.GetValue("_owner"));
                Assert.Null(originalPacket.GetValue("_buffer"));
                Assert.Null(expandedPacket.GetValue("_owner"));
            }
            finally
            {
                expandedPacket.Dispose();
            }
        }
        finally
        {
            originalPacket.Dispose();
        }
    }

    [Fact(DisplayName = "构造函数:Stream窃取MemoryStream内部缓冲区")]
    public void Constructor_WithExposableMemoryStream_ShouldStealBuffer()
    {
        var originalData = new Byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
        var ms = new MemoryStream();
        ms.Write(originalData, 0, originalData.Length);
        ms.Position = 0;

        Assert.True(ms.TryGetBuffer(out var seg));

        var packet = new OwnerPacket(ms);

        Assert.Same(seg.Array, packet.Buffer);
        Assert.Equal(seg.Offset, packet.Offset);
        Assert.Equal(seg.Count, packet.Length);

        var span = packet.GetSpan();
        for (var i = 0; i < originalData.Length; i++)
        {
            Assert.Equal(originalData[i], span[i]);
        }

        Assert.Equal(0, ms.Position);

        packet.Dispose();
    }

    [Fact(DisplayName = "构造函数:Stream窃取时Position参与偏移计算")]
    public void Constructor_WithExposableMemoryStreamAndPosition_ShouldUsePosition()
    {
        var ms = new MemoryStream();
        for (var i = 0; i < 10; i++) ms.WriteByte((Byte)(0xA0 + i));
        ms.Position = 3;

        Assert.True(ms.TryGetBuffer(out var seg));

        var packet = new OwnerPacket(ms);

        Assert.Same(seg.Array, packet.Buffer);
        Assert.Equal(seg.Offset + 3, packet.Offset);
        Assert.Equal(7, packet.Length);

        var span = packet.GetSpan();
        for (var i = 0; i < 7; i++)
        {
            Assert.Equal((Byte)(0xA3 + i), span[i]);
        }

        Assert.Equal(3, ms.Position);

        packet.Dispose();
    }

    [Fact(DisplayName = "构造函数:Stream在不可窃取时走池拷贝")]
    public void Constructor_WithNonExposableMemoryStream_ShouldCopyFromPool()
    {
        var originalData = new Byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
        var ms = new MemoryStream(originalData);
        ms.Position = 0;

        Assert.False(ms.TryGetBuffer(out _));

        var packet = new OwnerPacket(ms);

        Assert.NotSame(originalData, packet.Buffer);
        Assert.Equal(5, packet.Length);

        var span = packet.GetSpan();
        for (var i = 0; i < 5; i++)
        {
            Assert.Equal(originalData[i], span[i]);
        }

        Assert.Equal(0, ms.Position);

        packet.Dispose();
    }

    [Fact(DisplayName = "构造函数:Stream大数据池拷贝完整性")]
    public void Constructor_WithLargeNonExposableMemoryStream_ShouldCopyIntegrity()
    {
        var largeData = new Byte[2000];
        for (var i = 0; i < largeData.Length; i++) largeData[i] = (Byte)(i % 256);

        var ms = new MemoryStream(largeData);

        var packet = new OwnerPacket(ms);
        Assert.Equal(2000, packet.Length);
        Assert.NotSame(largeData, packet.Buffer);

        var span = packet.GetSpan();
        for (var i = 0; i < 2000; i++)
        {
            Assert.Equal(largeData[i], span[i]);
        }

        Assert.Equal(0, ms.Position);
        packet.Dispose();
    }

    [Fact(DisplayName = "构造函数:从空流创建")]
    public void Constructor_WithEmptyStream_ShouldCreateEmptyPacket()
    {
        var ms1 = new MemoryStream(Array.Empty<Byte>());
        var packet1 = new OwnerPacket(ms1);
        Assert.Equal(0, packet1.Length);
        packet1.Dispose();

        var ms2 = new MemoryStream();
        Assert.True(ms2.TryGetBuffer(out _));
        var packet2 = new OwnerPacket(ms2);
        Assert.Equal(0, packet2.Length);
        packet2.Dispose();
    }

    #endregion

    #region 索引器测试

    [Fact(DisplayName = "索引器:正常读写操作")]
    public void Indexer_NormalAccess_ShouldWorkCorrectly()
    {
        using var packet = new OwnerPacket(100);
        
        packet[50] = (Byte)'X';
        Assert.Equal((Byte)'X', packet[50]);
    }

    [Fact(DisplayName = "索引器:链式包跨段访问")]
    public void Indexer_ChainedPackets_ShouldAccessAcrossSegments()
    {
        using var packet1 = new OwnerPacket(50);
        using var packet2 = new OwnerPacket(50);
        packet1.Next = packet2;

        packet1[25] = 0x11;
        packet1[75] = 0x22;

        Assert.Equal(0x11, packet1[25]);
        Assert.Equal(0x22, packet1[75]);
        Assert.Equal(0x22, packet2[25]);
    }

    #endregion

    #region 内存访问测试

    [Fact(DisplayName = "GetSpan:应返回正确的内存片段")]
    public void GetSpan_ShouldReturnCorrectSpan()
    {
        using var packet = new OwnerPacket(100);
        packet.GetSpan().Fill(0x42);

        var span = packet.GetSpan();

        Assert.Equal(100, span.Length);
        Assert.True(span.ToArray().All(b => b == 0x42));
    }

    [Fact(DisplayName = "GetMemory:应返回正确的内存块")]
    public void GetMemory_ShouldReturnCorrectMemory()
    {
        using var packet = new OwnerPacket(100);
        packet.GetSpan().Fill(0x55);

        var memory = packet.GetMemory();

        Assert.Equal(100, memory.Length);
        Assert.True(memory.Span.ToArray().All(b => b == 0x55));
    }

    [Fact(DisplayName = "TryGetArray:应成功获取数组段")]
    public void TryGetArray_ShouldReturnArraySegment()
    {
        using var packet = new OwnerPacket(100);

        var success = packet.TryGetArray(out var segment);

        Assert.True(success);
        Assert.Same(packet.Buffer, segment.Array);
        Assert.Equal(packet.Offset, segment.Offset);
        Assert.Equal(packet.Length, segment.Count);
    }

    #endregion

    #region 大小调整测试

    [Fact(DisplayName = "Resize:调整到更小大小")]
    public void Resize_ToSmallerSize_ShouldAdjustLength()
    {
        using var packet = new OwnerPacket(100);

        var result = packet.Resize(50);

        Assert.Same(packet, result);
        Assert.Equal(50, packet.Length);
    }

    [Fact(DisplayName = "Resize:保持相同大小")]
    public void Resize_ToSameSize_ShouldKeepLength()
    {
        using var packet = new OwnerPacket(100);

        var result = packet.Resize(100);

        Assert.Same(packet, result);
        Assert.Equal(100, packet.Length);
    }

    #endregion

    #region 切片操作测试

    [Fact(DisplayName = "Slice:基本切片操作")]
    public void Slice_BasicOperation_ShouldCreateNewPacket()
    {
        using var packet = new OwnerPacket(100);
        packet.GetSpan().Fill(0x42);
        var buffer = packet.Buffer;

        using var sliced = packet.Slice(20, 30);

        Assert.NotNull(sliced);
        Assert.Same(buffer, sliced.Buffer);
        Assert.Equal(20, sliced.Offset);
        Assert.Equal(30, sliced.Length);

        // 共享语义:源实例保持可用,双方共享同一引用计数对象
        Assert.NotNull(packet.GetValue("_owner"));
        Assert.NotNull(packet.GetValue("_buffer"));
        Assert.Same(packet.GetValue("_owner"), sliced!.GetValue("_owner"));
        Assert.Equal(2, packet.RefCount);
        Assert.Equal(0x42, sliced[0]);
    }

    [Fact(DisplayName = "RefCount:追踪共享句柄数量")]
    public void RefCount_ShouldTrackSharedHandles()
    {
        var packet = new OwnerPacket(100);
        Assert.Equal(1, packet.RefCount);

        var first = packet.Slice(0, 50);
        var second = packet.Slice(50, 50);

        Assert.NotNull(first);
        Assert.NotNull(second);
        Assert.Equal(3, packet.RefCount);
        Assert.Equal(3, first!.RefCount);

        first.Dispose();
        Assert.Equal(2, packet.RefCount);

        second!.Dispose();
        Assert.Equal(1, packet.RefCount);

        packet.Dispose();
        Assert.Equal(0, packet.RefCount);
    }

    [Fact(DisplayName = "Slice:切片到末尾")]
    public void Slice_ToEnd_ShouldSliceToEndOfPacket()
    {
        using var packet = new OwnerPacket(100);

        using var sliced = packet.Slice(30, -1);

        Assert.NotNull(sliced);
        Assert.Equal(30, sliced.Offset);
        Assert.Equal(70, sliced.Length);
        Assert.Equal(2, packet.RefCount);
    }

    [Fact(DisplayName = "Slice:IOwnerPacket 接口返回拥有句柄,可 using 释放")]
    public void Slice_IOwnerPacketInterface_ShouldReturnOwnedHandle()
    {
        var packet = new OwnerPacket(100);
        packet.GetSpan().Fill(0x42);

        IOwnerPacket owner = packet;
        using (var view = owner.Slice(20, 30))
        {
            Assert.IsType<OwnerPacket>(view);
            Assert.Equal(30, view.Length);
            Assert.Equal(0x42, view.GetSpan()[0]);
            Assert.Equal(0x42, view[19]);

            // 视图与源各自持有引用,双方同时可用
            Assert.Equal(2, packet.RefCount);
        }

        // 视图释放后源仍可使用
        Assert.Equal(1, packet.RefCount);
        Assert.Equal(0x42, packet[0]);

        packet.Dispose();
    }

    [Fact(DisplayName = "Slice:经 IPacket 接口访问保持原有契约")]
    public void Slice_IPacketInterface_ShouldKeepContract()
    {
        var packet = new OwnerPacket(100);

        // 接口访问仍返回 IPacket,切片结果实际是拥有句柄
        IPacket view = ((IPacket)packet).Slice(10, 20);

        Assert.IsAssignableFrom<IOwnerPacket>(view);
        Assert.Equal(20, view.Length);
        Assert.Equal(2, packet.RefCount);

        view.TryDispose();
        Assert.Equal(1, packet.RefCount);

        packet.Dispose();
    }

    /// <summary>链式共享切片只递增窗口覆盖段的引用计数,窗口外的段保持不变</summary>
    /// <remarks>
    /// 场景:first(2字节) + second(8字节) 组成链,从偏移 4 开始切片——起点落在 second 段内。
    /// 期望:second 引用计数 +1(共享),first 保持不变;各句柄各自释放,最后一个归零归还池。
    /// </remarks>
    [Fact(DisplayName = "Slice:链式共享切片只递增窗口覆盖段的引用计数")]
    public void Slice_ChainShare_ShouldOnlyAddRefCoveredSegments()
    {
        var first = new OwnerPacket(2);
        var second = new OwnerPacket(8);
        first.Next = second;

        var firstOwner = first.GetValue("_owner");
        var secondOwner = second.GetValue("_owner");

        var slice = first.Slice(4, -1);

        Assert.NotNull(slice);
        Assert.Equal(6, slice!.Total);

        // 窗口外的 first:引用计数不变;second 在窗口内:引用计数 +1
        Assert.Equal(1, (Int32)firstOwner!.GetValue("_refCount")!);
        Assert.Equal(2, (Int32)secondOwner!.GetValue("_refCount")!);
        Assert.Same(secondOwner, slice.GetValue("_owner"));

        // 切片独立释放:second 回到 1
        slice.Dispose();
        Assert.Equal(1, (Int32)secondOwner.GetValue("_refCount")!);

        // 源链释放:链式递归归零
        first.Dispose();
        Assert.Equal(0, (Int32)firstOwner.GetValue("_refCount")!);
        Assert.Equal(0, (Int32)secondOwner.GetValue("_refCount")!);
    }

    /// <summary>链式共享切片:窗口未覆盖的尾部段引用计数不变,由源链继续持有</summary>
    /// <remarks>窗口只覆盖 first 段时,second 整段在窗口之外,不参与引用计数变化。</remarks>
    [Fact(DisplayName = "Slice:链式共享切片不触碰窗口外的尾部段")]
    public void Slice_ChainShare_ShouldNotTouchTailSegment()
    {
        var first = new OwnerPacket(2);
        var second = new OwnerPacket(8);
        first.Next = second;

        var firstOwner = first.GetValue("_owner");
        var secondOwner = second.GetValue("_owner");

        var slice = first.Slice(0, 2);

        Assert.NotNull(slice);
        Assert.Equal(2, slice!.Total);
        Assert.Equal(2, (Int32)firstOwner!.GetValue("_refCount")!);
        Assert.Equal(1, (Int32)secondOwner!.GetValue("_refCount")!);

        slice.Dispose();
        first.Dispose();
        Assert.Equal(0, (Int32)firstOwner.GetValue("_refCount")!);
        Assert.Equal(0, (Int32)secondOwner.GetValue("_refCount")!);
    }

    /// <summary>共享切片不改变源实例:长度、链、数据访问全部保持可用</summary>
    [Fact(DisplayName = "Slice:共享切片不改变源实例")]
    public void Slice_ShouldNotVoidSource()
    {
        var single = new OwnerPacket(100);
        var slice = single.Slice(10, 20);

        Assert.NotNull(slice);
        Assert.Equal(100, single.Length);
        Assert.Equal(100, single.Total);
        Assert.NotNull(single.GetValue("_buffer"));
        Assert.NotNull(single.GetValue("_owner"));
        Assert.Equal(20, slice!.Length);

        slice.Dispose();
        single.Dispose();
    }

    /// <summary>共享句柄与原实例同时持有引用,各自释放,最后一个释放才归还</summary>
    [Fact(DisplayName = "Slice:共享句柄双方独立使用与释放")]
    public void Slice_TwoHandles_ShouldBeIndependent()
    {
        var packet = new OwnerPacket(100);
        packet.GetSpan().Fill(0x42);

        var shared = packet.Slice(0);
        var owner = packet.GetValue("_owner");

        // 双方引用计数为 2,均可读取
        Assert.Equal(2, (Int32)owner!.GetValue("_refCount")!);
        Assert.Equal(0x42, shared[10]);
        Assert.Equal(0x42, packet[10]);

        // 先释放共享句柄:源仍可使用
        shared.TryDispose();
        Assert.Equal(1, (Int32)owner.GetValue("_refCount")!);
        Assert.Equal(0x42, packet[10]);

        // 再释放源:归零归还
        packet.Dispose();
        Assert.Equal(0, (Int32)owner.GetValue("_refCount")!);
    }

    /// <summary>共享窗口可跨链段,按段递增引用计数</summary>
    [Fact(DisplayName = "Slice:窗口共享跨链段递增引用计数")]
    public void Slice_Window_ShouldCoverChainedNodes()
    {
        var first = new OwnerPacket(4);
        var second = new OwnerPacket(8);
        first.Next = second;
        first.GetSpan().Fill(0x11);
        second.GetSpan().Fill(0x22);

        var shared = first.Slice(2, 8);

        Assert.Equal(8, shared.Total);
        Assert.Equal(0x11, shared[0]);
        Assert.Equal(0x22, shared[2]);
        Assert.Equal(0x22, shared[7]);

        var firstOwner = first.GetValue("_owner");
        var secondOwner = second.GetValue("_owner");
        Assert.Equal(2, (Int32)firstOwner!.GetValue("_refCount")!);
        Assert.Equal(2, (Int32)secondOwner!.GetValue("_refCount")!);

        // 共享句柄独立释放:源链引用保持不变
        shared.TryDispose();
        Assert.Equal(1, (Int32)firstOwner.GetValue("_refCount")!);
        Assert.Equal(1, (Int32)secondOwner.GetValue("_refCount")!);

        // 源链释放:链式递归归零
        first.Dispose();
        Assert.Equal(0, (Int32)firstOwner.GetValue("_refCount")!);
        Assert.Equal(0, (Int32)secondOwner.GetValue("_refCount")!);
    }

    /// <summary>原地前移窗口:不分配、不调整引用计数</summary>
    [Fact(DisplayName = "Skip:原地前移窗口")]
    public void Skip_ShouldAdvanceWindowInPlace()
    {
        var packet = new OwnerPacket(100);
        packet.GetSpan().Fill(0x42);
        var offset = packet.Offset;

        var rs = packet.Skip(10);

        Assert.Same(packet, rs);
        Assert.Equal(offset + 10, packet.Offset);
        Assert.Equal(90, packet.Length);
        Assert.Equal(90, packet.Total);
        Assert.Equal(0x42, packet[0]);
        Assert.Throws<ArgumentOutOfRangeException>(() => packet.Skip(91));
    }

    #endregion

    #region 属性测试

    [Fact(DisplayName = "Total:单个包应返回Length")]
    public void Total_SinglePacket_ShouldReturnLength()
    {
        using var packet = new OwnerPacket(100);

        Assert.Equal(100, packet.Total);
        Assert.Equal(packet.Length, packet.Total);
    }

    [Fact(DisplayName = "Total:链式包应返回所有段的总长度")]
    public void Total_ChainedPackets_ShouldReturnTotalLength()
    {
        using var packet1 = new OwnerPacket(50);
        using var packet2 = new OwnerPacket(30);
        using var packet3 = new OwnerPacket(20);
        packet1.Next = packet2;
        packet2.Next = packet3;

        Assert.Equal(100, packet1.Total);
    }

    #endregion

    #region 字符串表示测试

    [Fact(DisplayName = "ToString:应返回格式化的字符串表示")]
    public void ToString_ShouldReturnFormattedString()
    {
        using var packet = new OwnerPacket(100);

        var result = packet.ToString();

        Assert.Matches(@"\[\d+\]\(0, 100\)<100>", result);
    }

    #endregion

    #region 性能测试

    [Fact(DisplayName = "性能:切片操作应该零拷贝")]
    public void Performance_Slice_ShouldBeZeroCopy()
    {
        using var packet = new OwnerPacket(1000);
        packet.GetSpan().Fill(0x42);

        var slice1 = packet.Slice(100, 200);
        var slice2 = slice1.Slice(50, 100);

        // 验证数据一致性(间接验证零拷贝)
        Assert.Equal(0x42, slice2[0]);
        Assert.Equal(0x42, slice2[99]);

        slice2.TryDispose();
        slice1.TryDispose();
    }

    #endregion

    #region 边界条件测试

    [Fact(DisplayName = "边界条件:空包处理")]
    public void EdgeCase_EmptyPacket_ShouldHandleCorrectly()
    {
        using var packet = new OwnerPacket(0);

        Assert.Equal(0, packet.Length);
        Assert.Equal(0, packet.Total);
        Assert.Empty(packet.GetSpan().ToArray());
        Assert.Empty(packet.GetMemory().ToArray());
    }

    [Fact(DisplayName = "边界条件:大内存包处理")]
    public void EdgeCase_LargePacket_ShouldHandleCorrectly()
    {
        using var packet = new OwnerPacket(1024 * 1024); // 1MB

        Assert.Equal(1024 * 1024, packet.Length);
        Assert.True(packet.Buffer.Length >= 1024 * 1024);

        // 测试边界访问
        packet[0] = 0x11;
        packet[packet.Length - 1] = 0x22;
        Assert.Equal(0x11, packet[0]);
        Assert.Equal(0x22, packet[packet.Length - 1]);
    }

    #endregion

    #region 内存管理测试

    [Fact(DisplayName = "Dispose:应归还缓冲区")]
    public void Dispose_ShouldReturnBufferToPool()
    {
        var packet = new OwnerPacket(100);
        Assert.NotNull(packet.GetValue("_owner"));

        packet.Dispose();

        Assert.Null(packet.GetValue("_owner"));
        Assert.Null(packet.GetValue("_buffer"));
        Assert.Null(packet.Next);
    }

    [Fact(DisplayName = "Dispose:多次调用应幂等")]
    public void Dispose_MultipleCalls_ShouldBeIdempotent()
    {
        var packet = new OwnerPacket(100);

        // 多次 Dispose 不应抛异常
        packet.Dispose();
        packet.Dispose();
        packet.Dispose();

        Assert.Null(packet.GetValue("_owner"));
    }

    [Fact(DisplayName = "Dispose:无所有权时不归还缓冲区")]
    public void Dispose_WithoutOwnership_ShouldNotReturnBuffer()
    {
        var buffer = new Byte[200];
        var packet = new OwnerPacket(buffer, 10, 50, false);

        // 无所有权的 Dispose 不应报错
        packet.Dispose();

        Assert.Null(packet.GetValue("_owner"));
    }

    [Fact(DisplayName = "Dispose:应释放链式后续节点")]
    public void Dispose_ShouldDisposeChainedNextPackets()
    {
        var packet1 = new OwnerPacket(50);
        var packet2 = new OwnerPacket(50);
        packet1.Next = packet2;

        packet1.Dispose();

        Assert.Null(packet1.Next);
        Assert.Null(packet2.GetValue("_owner"));
    }

    [Fact(DisplayName = "Dispose:借用视图头节点释放链上拥有尾链")]
    public void Dispose_BorrowHeadReleasesOwnedTail()
    {
        var buffer = new Byte[] { 1, 2, 3, 4 };
        var owned = new OwnerPacket(8);
        var head = new OwnerPacket(buffer, 0, buffer.Length, false) { Next = owned };

        // 从借用头(不持引用)切出共享切片:头节点为借用、尾节点递增引用计数
        var slice = head.Slice(0, -1);
        Assert.Equal(2, owned.RefCount);

        // 借用视图头 Dispose 也必须释放链上拥有节点,否则引用计数永不归零
        slice.TryDispose();
        Assert.Equal(1, owned.RefCount);

        owned.Dispose();
        Assert.Equal(0, owned.RefCount);
        head.TryDispose();
    }

    [Fact(DisplayName = "释放后访问:GetSpan 应抛异常")]
    public void AfterDispose_GetSpan_ShouldThrow()
    {
        var packet = new OwnerPacket(100);
        packet.Dispose();

        Assert.Throws<ObjectDisposedException>(() => packet.GetSpan());
    }

    [Fact(DisplayName = "释放后访问:GetMemory 应抛异常")]
    public void AfterDispose_GetMemory_ShouldThrow()
    {
        var packet = new OwnerPacket(100);
        packet.Dispose();

        Assert.Throws<ObjectDisposedException>(() => packet.GetMemory());
    }

    [Fact(DisplayName = "释放后访问:索引器应抛异常")]
    public void AfterDispose_Indexer_ShouldThrow()
    {
        var packet = new OwnerPacket(100);
        packet.Dispose();

        Assert.Throws<ObjectDisposedException>(() => packet[0]);
    }

    [Fact(DisplayName = "内存管理:ArrayPool复用验证")]
    public void MemoryManagement_ArrayPoolReuse_ShouldReuseBuffers()
    {
        var packets = new List<OwnerPacket>();

        // 创建多个相同大小的包
        for (var i = 0; i < 10; i++)
        {
            var packet = new OwnerPacket(1024);
            packets.Add(packet);
        }

        // 释放前几个,归还内存池
        foreach (var p in packets.Take(5))
        {
            p.Dispose();
        }

        // 创建新的包,应该能复用缓冲区
        using var newPacket = new OwnerPacket(1024);

        Assert.NotNull(newPacket.Buffer);
        Assert.True(newPacket.Buffer.Length >= 1024);

        // 清理剩余的包
        foreach (var p in packets.Skip(5))
        {
            p.Dispose();
        }
    }

#if DEBUG || OWNERPACKET_FINALIZER
    // 注:本测试依赖 NewLife.Core 以相同条件编译析构兜底(默认 Debug 构建成立)
    [Fact(DisplayName = "析构兜底:漏释放的句柄在 GC 时释放引用")]
    public void Finalizer_ShouldReleaseReference()
    {
        var owner = LeakOne();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        // 析构已释放漏释放句柄的引用
        Assert.Equal(0, (Int32)owner.GetValue("_refCount")!);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static Object LeakOne()
    {
        var packet = new OwnerPacket(100);
        return packet.GetValue("_owner")!;
    }
#endif

    [Fact(DisplayName = "Detach:脱手后缓冲不归还,句柄作废")]
    public void Detach_ShouldAbandonHandleWithoutReturn()
    {
        var packet = new OwnerPacket(100);
        var owner = packet.GetValue("_owner");

        packet.Detach();

        Assert.Null(packet.GetValue("_owner"));
        Assert.Null(packet.GetValue("_buffer"));
        Assert.Null(packet.Next);
        Assert.Equal(0, packet.Length);
        Assert.Throws<ObjectDisposedException>(() => packet.GetSpan());

        // 引用计数保持 1:缓冲仍处于借出状态,归还责任已转移给调用方
        Assert.Equal(1, (Int32)owner!.GetValue("_refCount")!);
    }

    [Fact(DisplayName = "Detach:GC 兜底不归还借用中的缓冲")]
    public void Detach_ShouldSuppressFinalizer()
    {
        var owner = DetachAndForget();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        // 析构被抑制:缓冲仍处于借出状态,未被归还
        Assert.Equal(1, (Int32)owner.GetValue("_refCount")!);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static Object DetachAndForget()
    {
        var packet = new OwnerPacket(100);
        var owner = packet.GetValue("_owner")!;
        packet.Detach();
        return owner;
    }

    [Fact(DisplayName = "Detach:存在其它句柄时抛出异常且不改变状态")]
    public void Detach_WithOtherHandles_ShouldThrow()
    {
        var packet = new OwnerPacket(100);
        var slice = packet.Slice(0, 50);

        Assert.Throws<InvalidOperationException>(() => packet.Detach());

        // 句柄保持可用:异常不改变任何状态
        Assert.NotNull(packet.GetValue("_owner"));
        Assert.Equal(2, packet.RefCount);

        slice.TryDispose();
        packet.Dispose();
    }

    [Fact(DisplayName = "Detach:重复调用幂等,借用视图无操作")]
    public void Detach_MultipleCalls_ShouldBeIdempotent()
    {
        var packet = new OwnerPacket(100);
        packet.Detach();
        packet.Detach();

        Assert.Null(packet.GetValue("_owner"));

        // 借用视图不持引用,脱手无操作
        var view = new OwnerPacket(new Byte[10], 0, 10, false);
        view.Detach();
        Assert.NotNull(view.GetValue("_buffer"));
        view.TryDispose();
    }

    #endregion

    #region 接收环复用
    [Fact(DisplayName = "Rebind:独占句柄重绑到新窗口,缓冲与引用计数保持")]
    public void Rebind_ExclusiveHandle_ShouldRetarget()
    {
        var buffer = new Byte[16];
        var packet = new OwnerPacket(buffer, 0, 4, true);

        packet.Rebind(buffer, 2, 8);

        Assert.Same(buffer, packet.Buffer);
        Assert.Equal(2, packet.Offset);
        Assert.Equal(8, packet.Length);
        Assert.Equal(1, packet.RefCount);

        // 重绑后仍按新窗口读写
        packet[0] = 42;
        Assert.Equal(42, buffer[2]);

        packet.Dispose();
    }

    [Fact(DisplayName = "Rebind:仍有其它持有者时抛异常且不改变窗口")]
    public void Rebind_WithOtherHandles_ShouldThrow()
    {
        var packet = new OwnerPacket(new Byte[16], 0, 4, true);
        using var extra = packet.Slice(0, 2);

        Assert.Throws<InvalidOperationException>(() => packet.Rebind(packet.Buffer, 2, 8));

        // 异常不改变任何状态:其它持有者还在读这块缓冲,重绑会让接收数据把它覆盖掉
        Assert.Equal(0, packet.Offset);
        Assert.Equal(4, packet.Length);
        Assert.Equal(2, packet.RefCount);

        packet.Dispose();
    }
    #endregion
}