using System.Collections.Concurrent; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Numbering; using Xunit; using Xunit.Abstractions; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// Phase 4A 取号并发测试(同进程;InMemory provider)。 /// **可现在证明**:同进程高并发下取号结果不重复、批内连续、重试有界。 /// **不能声称**:多实例 + 真实 MySQL 最终唯一 / DB UNIQUE 实拒(→ Phase 4B)。 /// public class BusinessNumberConcurrencyTests { private readonly ITestOutputHelper _out; public BusinessNumberConcurrencyTests(ITestOutputHelper output) => _out = output; private static InMemoryBusinessNumberProvider NewProvider() => new InMemoryBusinessNumberProvider( new[] { new BusinessNumberRule { RuleCode = "IQC_RCT", Prefix1 = "IR", DateType = "YYYYMMDD", IsDateType = true, MaxValue = 9999999, IniValue = 1, }, }, clock: () => new DateTime(2026, 8, 8)); [Fact] public async Task NextBatch_1000Concurrent_AllUnique_NoDuplicate() { const int n = 1000; var provider = NewProvider(); var bag = new ConcurrentBag(); var tasks = Enumerable.Range(0, n).Select(_ => Task.Run(async () => { var b = await provider.NextBatchAsync("IQC_RCT", "8010", 1, "tester"); bag.Add(b[0]); })); await Task.WhenAll(tasks); var all = bag.ToList(); var unique = all.Distinct().Count(); var duplicates = all.Count - unique; _out.WriteLine($"[取号并发] 总请求={all.Count} 唯一={unique} 重复={duplicates}"); Assert.Equal(n, all.Count); Assert.Equal(n, unique); Assert.Equal(0, duplicates); } [Fact] public async Task NextBatch_ReservesContiguousBlock() { var provider = NewProvider(); var batch = await provider.NextBatchAsync("IQC_RCT", "8010", 5, "tester"); Assert.Equal(5, batch.Count); Assert.Equal(5, batch.Distinct().Count()); // 批内连续:末位流水依次 +1 Assert.EndsWith("0000001", batch[0]); Assert.EndsWith("0000005", batch[4]); } [Fact] public async Task NextBatch_DifferentDomain_IndependentCounters() { var provider = NewProvider(); var a = (await provider.NextBatchAsync("IQC_RCT", "8010", 1, "t"))[0]; var b = (await provider.NextBatchAsync("IQC_RCT", "9010", 1, "t"))[0]; // 不同 domain 各自计数,均从 1 开始 → 号相同但作用域不同(说明按 domain 隔离) Assert.EndsWith("0000001", a); Assert.EndsWith("0000001", b); } [Fact] public async Task NextWithUniqueRetry_DuplicateThenSuccess_BoundedAttempts() { var svc = new BusinessNumberService(NewProvider()); var calls = 0; // 前 2 次模拟撞 DB UNIQUE(返回 false),第 3 次成功 var r = await svc.NextWithUniqueRetryAsync("IQC_RCT", "8010", "t", tryConsume: _ => { calls++; return Task.FromResult(calls >= 3); }, maxRetry: 5); _out.WriteLine($"[取号重试] success={r.Success} attempts={r.Attempts}"); Assert.True(r.Success); Assert.Equal(3, r.Attempts); } [Fact] public async Task NextWithUniqueRetry_AllDuplicate_FailsBounded_NoInfiniteLoop() { var svc = new BusinessNumberService(NewProvider()); var calls = 0; var r = await svc.NextWithUniqueRetryAsync("IQC_RCT", "8010", "t", tryConsume: _ => { calls++; return Task.FromResult(false); }, maxRetry: 5); _out.WriteLine($"[取号重试-全撞] success={r.Success} attempts={r.Attempts} calls={calls}"); Assert.False(r.Success); // 有界失败,不无限循环 Assert.Equal(5, r.Attempts); Assert.Equal(5, calls); } }