From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理
---
tests/Aging.MvpTests/DbLeaseStoreTests.cs | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 160 insertions(+), 0 deletions(-)
diff --git a/tests/Aging.MvpTests/DbLeaseStoreTests.cs b/tests/Aging.MvpTests/DbLeaseStoreTests.cs
new file mode 100644
index 0000000..ef1b2d0
--- /dev/null
+++ b/tests/Aging.MvpTests/DbLeaseStoreTests.cs
@@ -0,0 +1,160 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Core;
+using Aging.ResourceContracts;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+ public class DbLeaseStoreTests : IDisposable
+ {
+ private readonly string _dbPath;
+ private readonly DbLeaseStore _store;
+
+ public DbLeaseStoreTests()
+ {
+ _dbPath = Path.Combine(Path.GetTempPath(),
+ "arb_test_" + Guid.NewGuid().ToString("N") + ".db");
+ _store = new DbLeaseStore(_dbPath, "local-main");
+ }
+ public void Dispose() { try { File.Delete(_dbPath); File.Delete(_dbPath + "-wal"); File.Delete(_dbPath + "-shm"); } catch { } }
+
+ private AcquireRequest Req(string key, OwnerTypeV3 type = OwnerTypeV3.Maintenance,
+ int ttl = 30) => new AcquireRequest
+ {
+ OwnerId = "test-owner", OwnerType = type,
+ ProcessInstanceId = Guid.NewGuid().ToString("N"),
+ ResourceKeys = new[] { key }, TtlSeconds = ttl
+ };
+
+ [Fact]
+ public void Acquire_FirstOwner_Succeeds()
+ {
+ var r = _store.Acquire(Req("can/can02"));
+ Assert.Equal(AcquireResultCode.Acquired, r.Code);
+ Assert.Equal(1, r.Grants[0].Generation);
+ }
+
+ [Fact]
+ public void Acquire_WhileActiveByOther_ReturnsConflictWithHolder()
+ {
+ _store.Acquire(Req("can/can02"));
+ var second = new AcquireRequest
+ {
+ OwnerId = "other-owner", OwnerType = OwnerTypeV3.Production,
+ ProcessInstanceId = "x", ResourceKeys = new[] { "can/can02" }
+ };
+ var r = _store.Acquire(second);
+ Assert.Equal(AcquireResultCode.ConflictHeld, r.Code);
+ Assert.Equal("test-owner", r.Holder.OwnerId);
+ }
+
+ [Fact]
+ public void TwoIndependentStores_SameFile_SimulatesTwoProcesses()
+ {
+ var storeB = new DbLeaseStore(_dbPath, "local-main");
+ var a = _store.Acquire(Req("psu/psu01/ch/5"));
+ var b = storeB.Acquire(new AcquireRequest
+ {
+ OwnerId = "W02", OwnerType = OwnerTypeV3.Production,
+ ProcessInstanceId = "p2", ResourceKeys = new[] { "psu/psu01/ch/5" }
+ });
+ Assert.Equal(AcquireResultCode.Acquired, a.Code);
+ Assert.Equal(AcquireResultCode.ConflictHeld, b.Code);
+ }
+
+ [Fact]
+ public void Concurrent_50Threads_OnlyOneWins()
+ {
+ int winners = 0;
+ Parallel.For(0, 50, _ =>
+ {
+ var s = new DbLeaseStore(_dbPath, "local-main");
+ var r = s.Acquire(new AcquireRequest
+ {
+ OwnerId = Guid.NewGuid().ToString("N"), OwnerType = OwnerTypeV3.Production,
+ ProcessInstanceId = "p", ResourceKeys = new[] { "can/bus" }, WaitTimeoutMs = 3000
+ });
+ if (r.Code == AcquireResultCode.Acquired) Interlocked.Increment(ref winners);
+ });
+ Assert.Equal(1, winners);
+ }
+
+ [Fact]
+ public void ExpiredLease_EntersRecoveryRequired_NotAutoReclaim()
+ {
+ _store.Acquire(Req("can/can02", ttl: -1));
+ _store.SweepExpired();
+ var r = _store.Acquire(Req("can/can02", OwnerTypeV3.Production));
+ Assert.Equal(AcquireResultCode.RecoveryBlocked, r.Code);
+
+ Assert.True(_store.ApproveRecovery("can/can02", "{\"output\":\"off\"}", "admin"));
+ var again = _store.Acquire(Req("can/can02", OwnerTypeV3.Production));
+ Assert.Equal(AcquireResultCode.Acquired, again.Code);
+ Assert.Equal(2, again.Grants[0].Generation);
+ }
+
+ [Fact]
+ public void Renew_WithStaleGeneration_Fails()
+ {
+ var g = _store.Acquire(Req("can/can02")).Grants[0];
+ var stale = new LeaseGrant(g.AuthorityId, g.ResourceKey, g.LeaseId,
+ g.OwnerId, g.OwnerType, g.Epoch, 999, g.ExpiresUtcMs);
+ Assert.False(_store.Renew(stale, 30));
+ Assert.True(_store.Renew(g, 30));
+ }
+
+ [Fact]
+ public void BumpEpoch_InvalidatesAllActiveGrants()
+ {
+ var g = _store.Acquire(Req("can/can02")).Grants[0];
+ _store.BumpEpochForTest("arb.db 閲嶅惎妯℃嫙");
+ Assert.False(_store.Renew(g, 30));
+ }
+
+ [Fact]
+ public void GroupAcquire_PartialConflict_RollsBackAll()
+ {
+ _store.Acquire(Req("psu/b"));
+ var r = _store.Acquire(new AcquireRequest
+ {
+ OwnerId = "W09", OwnerType = OwnerTypeV3.Production, ProcessInstanceId = "p",
+ ResourceKeys = new[] { "psu/a", "psu/b", "psu/c" }
+ });
+ Assert.Equal(AcquireResultCode.ConflictHeld, r.Code);
+ Assert.Null(_store.ProbeConflict("psu/a"));
+ }
+
+ [Fact]
+ public void ExpiredLease_ReconcileAfterBoot_FreesResource_ForNewAcquire()
+ {
+ // 妯℃嫙鏃� owner 杩涚▼鏈噴鏀惧氨寮傚父閫�鍑猴細鐭� TTL 绉熺害娉勬紡鍚庤繃鏈�
+ var leaked = _store.Acquire(Req("can/can99", OwnerTypeV3.Maintenance, ttl: 1));
+ Assert.Equal(AcquireResultCode.Acquired, leaked.Code);
+ Thread.Sleep(1600);
+
+ // 鏈璐﹀墠锛氳繃鏈熺绾﹁繘鍏� RecoveryBlocked锛堟案涔呴樆濉炵幇鐘讹級
+ var blocked = _store.Acquire(new AcquireRequest
+ {
+ OwnerId = "new-owner", OwnerType = OwnerTypeV3.Maintenance,
+ ProcessInstanceId = "y", ResourceKeys = new[] { "can/can99" }
+ });
+ Assert.Equal(AcquireResultCode.RecoveryBlocked, blocked.Code);
+
+ // 鍚姩瀵硅处鑷剤锛圛NV-03 鑷姩鍖栧舰寮忥紝璇佹嵁钀� recovery_checkpoint锛�
+ Assert.True(_store.ReconcileAfterBoot("HOST/999") >= 1);
+ Assert.Equal(LeasePhase.Free, _store.PhaseOf("can/can99"));
+
+ // 瀵硅处鍚庡彲閲嶆柊鑾峰彇锛屼笖 Generation 鍙墠杩涗笉鍥為��
+ var reacquired = _store.Acquire(new AcquireRequest
+ {
+ OwnerId = "new-owner", OwnerType = OwnerTypeV3.Maintenance,
+ ProcessInstanceId = "y", ResourceKeys = new[] { "can/can99" }
+ });
+ Assert.Equal(AcquireResultCode.Acquired, reacquired.Code);
+ Assert.True(reacquired.Grants[0].Generation > leaked.Grants[0].Generation);
+ }
+ }
+}
--
Gitblit v1.9.1