wenxiangchao
3 天以前 b544d5cccd76735e034c8055918a50927b437474
tests/Aging.MvpTests/BrokerE2ETests.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Broker;
@@ -212,6 +213,192 @@
            _clientLease.Release(grant, "test-end");
        }
        // ==================== 并发与生命周期 ====================
        /// <summary>单客户端连续 5 次快速发送:验证 SessionGate 串行化不丢命令、租约不被误释放。</summary>
        [Fact]
        public async Task SingleClient_RapidSend5Times_AllSucceed_LeaseStaysActive()
        {
            var grant = GrabLease();
            var hash = DeviceConfigHash.Compute(_dataDb);
            using var driver = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
            await RetryAsync(() => driver.ConnectAsync(CancellationToken.None));
            for (int i = 0; i < 5; i++)
            {
                var tx = await driver.SendCommandAsync($"SEND:0x{i:X3}:AA", CancellationToken.None);
                Assert.Contains("OK:TX", tx);
            }
            // 5 次操作后租约仍 Active(未被意外释放或过期)
            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
            _clientLease.Release(grant, "test-end");
        }
        /// <summary>
        /// 两个 Broker 代理驱动同时持有同一设备的活跃租约,并发发送各 5 次。
        /// 由于 arb.db 以 ResourceKey 为主键只允许一行 Active,第二个租约通过直接 SQL 插入模拟
        /// (MVP 栅栏仅校验 Phase=Active + 未过期,不比对 OwnerId)。
        /// 验证 SessionGate(SemaphoreSlim=1) 将两个并发请求串行执行,两者均成功。
        /// </summary>
        [Fact]
        public async Task TwoClients_ConcurrentSend_BothSucceed_SessionGateSerializes()
        {
            // 客户端 A:经正常 Acquire 获取 Production 租约
            var grantA = GrabLease();
            var hash = DeviceConfigHash.Compute(_dataDb);
            using var driverA = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
            await RetryAsync(() => driverA.ConnectAsync(CancellationToken.None));
            // 客户端 B:直接往 arb.db 插入一条 Maintenance Active 租约行,模拟另一个持有者
            InsertDirectLeaseRow(DevId, "concurrent-client-B", OwnerTypeV3.Maintenance, ttlSeconds: 30);
            using var driverB = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
            await RetryAsync(() => driverB.ConnectAsync(CancellationToken.None));
            // 并发发送各 5 次
            var tasksA = Enumerable.Range(0, 5).Select(i =>
                driverA.SendCommandAsync($"SEND:0x{i:X3}:AA", CancellationToken.None)).ToArray();
            var tasksB = Enumerable.Range(0, 5).Select(i =>
                driverB.SendCommandAsync($"SEND:0x{i + 0x10:X3}:BB", CancellationToken.None)).ToArray();
            var results = await Task.WhenAll(tasksA.Concat(tasksB));
            Assert.All(results, r => Assert.Contains("OK:TX", r));
            // 两个租约行均保持 Active
            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
            _clientLease.Release(grantA, "test-end-A");
        }
        /// <summary>
        /// 两个 Production 客户端竞争同一设备租约:第一个成功获取,第二个应得到 ConflictHeld。
        /// </summary>
        [Fact]
        public void TwoProductionClients_SecondGetsConflictHeld()
        {
            var grantA = GrabLease();
            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
            // 第二个客户端(不同 OwnerId,同 OwnerType=Production)
            var secondLease = new DbLeaseStore(_arbDb, "second-client");
            var outcome = secondLease.Acquire(new AcquireRequest
            {
                OwnerId = "competing-client",
                OwnerType = OwnerTypeV3.Production,
                ResourceKeys = new[] { DevId },
                TtlSeconds = 30,
                WaitTimeoutMs = 0
            });
            Assert.Equal(AcquireResultCode.ConflictHeld, outcome.Code);
            Assert.NotNull(outcome.Holder);
            _clientLease.Release(grantA, "test-end");
        }
        /// <summary>
        /// 完整生命周期:Acquire → Connect → Send → Release → 再 Acquire → 再 Send。
        /// 验证租约释放后资源回到 Free,可被重新获取并继续使用。
        /// </summary>
        [Fact]
        public async Task LeaseLifecycle_AcquireSendReleaseAcquireSendAgain_AllSucceed()
        {
            var hash = DeviceConfigHash.Compute(_dataDb);
            // 第一轮:获取租约 → 连接 → 发送 → 释放
            LeaseGrant grant1;
            {
                var outcome = _clientLease.Acquire(new AcquireRequest
                {
                    OwnerId = "lifecycle-1",
                    OwnerType = OwnerTypeV3.Production,
                    ResourceKeys = new[] { DevId },
                    TtlSeconds = 30
                });
                Assert.Equal(AcquireResultCode.Acquired, outcome.Code);
                grant1 = outcome.Grants[0];
            }
            using (var driver1 = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash))
            {
                await RetryAsync(() => driver1.ConnectAsync(CancellationToken.None));
                var tx = await driver1.SendCommandAsync("SEND:0x100:AA", CancellationToken.None);
                Assert.Contains("OK:TX", tx);
            }
            Assert.True(_clientLease.Release(grant1, "lifecycle-1-end"));
            Assert.Equal(LeasePhase.Free, _clientLease.PhaseOf(DevId));
            // 第二轮:重新获取 → 连接 → 发送
            LeaseGrant grant2;
            {
                var outcome = _clientLease.Acquire(new AcquireRequest
                {
                    OwnerId = "lifecycle-2",
                    OwnerType = OwnerTypeV3.Production,
                    ResourceKeys = new[] { DevId },
                    TtlSeconds = 30
                });
                Assert.Equal(AcquireResultCode.Acquired, outcome.Code);
                grant2 = outcome.Grants[0];
            }
            using (var driver2 = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash))
            {
                await RetryAsync(() => driver2.ConnectAsync(CancellationToken.None));
                var tx = await driver2.SendCommandAsync("SEND:0x200:BB", CancellationToken.None);
                Assert.Contains("OK:TX", tx);
            }
            _clientLease.Release(grant2, "lifecycle-2-end");
        }
        /// <summary>
        /// 并发压力:单客户端 20 个 Task 同时发送,验证 SessionGate + 管道写入锁不崩溃。
        /// </summary>
        [Fact]
        public async Task SingleClient_ConcurrentSend20Tasks_AllSucceed()
        {
            var grant = GrabLease();
            var hash = DeviceConfigHash.Compute(_dataDb);
            using var driver = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
            await RetryAsync(() => driver.ConnectAsync(CancellationToken.None));
            var tasks = Enumerable.Range(0, 20).Select(i =>
                driver.SendCommandAsync($"SEND:0x{i:X3}:DD", CancellationToken.None)).ToArray();
            var results = await Task.WhenAll(tasks);
            Assert.All(results, r => Assert.Contains("OK:TX", r));
            Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
            _clientLease.Release(grant, "test-end");
        }
        // ==================== 辅助 ====================
        /// <summary>绕过 Acquire 互斥,直接向 arb.db 插入一条 Active 租约行(测试并发场景用)。</summary>
        private void InsertDirectLeaseRow(string deviceId, string ownerId, OwnerTypeV3 ownerType, int ttlSeconds)
        {
            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            using var conn = new System.Data.SQLite.SQLiteConnection(
                $"Data Source={_arbDb};Version=3;");
            conn.Open();
            using var cmd = conn.CreateCommand();
            cmd.CommandText = @"
                INSERT OR REPLACE INTO resource_state
                    (ResourceKey, Generation, Phase, LeaseId, OwnerId, OwnerType,
                     ProcessInstanceId, MachineName, HostProcessId, Epoch, Reason,
                     AcquiredUtcMs, ExpiresUtcMs, UpdatedUtcMs)
                VALUES (@key, 1, 1, @lid, @oid, @otype, '', 'local', 0, 1, 'direct-insert',
                        @now, @exp, @now)";
            cmd.Parameters.AddWithValue("@key", DbLeaseStore.NormKey(deviceId));
            cmd.Parameters.AddWithValue("@lid", "lease_" + Guid.NewGuid().ToString("N").Substring(0, 12));
            cmd.Parameters.AddWithValue("@oid", ownerId);
            cmd.Parameters.AddWithValue("@otype", (int)ownerType);
            cmd.Parameters.AddWithValue("@now", now);
            cmd.Parameters.AddWithValue("@exp", now + ttlSeconds * 1000L);
            cmd.ExecuteNonQuery();
        }
        private sealed class CanFrameish { public string Id; public string Data; public string Src; }
        [Fact]