using System.Net; using System.Net.Sockets; using Mbproxy.Proxy.Multiplexing; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; namespace Mbproxy.Tests.Proxy.Multiplexing; /// /// Unit tests for the Phase-10 . Covers the atomic /// attach-or-create primitive (load-bearing concurrency invariant), the per-entry /// max-parties cap (load-shedding safety valve), and concurrent attach correctness. /// [Trait("Category", "Unit")] public sealed class InFlightByKeyMapTests { private static UpstreamPipe MakePipe() { // The map only retains references to InterestedParty; it never reads pipe state. // A connected loopback socket satisfies the constructor contract. var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var c = new TcpClient(); c.Connect(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port); var s = listener.AcceptSocket(); listener.Stop(); return new UpstreamPipe(s, "PLC1", NullLogger.Instance); } private static InFlightRequest MakeRequest(InterestedParty party, byte fc = 0x03, ushort start = 100, ushort qty = 1, byte unit = 1) { // The factory uses a mutable List so the map can append on attach. var list = new List(capacity: 1) { party }; return new InFlightRequest( UnitId: unit, Fc: fc, StartAddress: start, Qty: qty, InterestedParties: list, SentAtUtc: DateTimeOffset.UtcNow); } [Fact] public async Task TryAttachOrCreate_NewKey_CallsFactory_ReturnsTrue_WasNewTrue() { var pipe = MakePipe(); try { var map = new InFlightByKeyMap(); var key = new CoalescingKey(1, 0x03, 100, 1); var party = new InterestedParty(pipe, OriginalTxId: 0x1234); int factoryCalls = 0; bool ok = map.TryAttachOrCreate( key, party, factory: () => { factoryCalls++; return MakeRequest(party); }, maxParties: 32, out var req, out bool wasNew); ok.ShouldBeTrue(); wasNew.ShouldBeTrue("a brand-new key must take the create branch"); factoryCalls.ShouldBe(1, "the factory must be called exactly once"); req.ShouldNotBeNull(); req.InterestedParties.Count.ShouldBe(1); map.Count.ShouldBe(1); } finally { await pipe.DisposeAsync(); } } [Fact] public async Task TryAttachOrCreate_ExistingKey_AppendsParty_ReturnsTrue_WasNewFalse() { var pipeA = MakePipe(); var pipeB = MakePipe(); try { var map = new InFlightByKeyMap(); var key = new CoalescingKey(1, 0x03, 100, 1); var partyA = new InterestedParty(pipeA, OriginalTxId: 0x1111); var partyB = new InterestedParty(pipeB, OriginalTxId: 0x2222); int factoryCalls = 0; map.TryAttachOrCreate(key, partyA, factory: () => { factoryCalls++; return MakeRequest(partyA); }, maxParties: 32, out var first, out bool firstWasNew); bool ok = map.TryAttachOrCreate(key, partyB, factory: () => { factoryCalls++; return MakeRequest(partyB); }, maxParties: 32, out var second, out bool secondWasNew); ok.ShouldBeTrue(); firstWasNew.ShouldBeTrue(); secondWasNew.ShouldBeFalse("the second attach must coalesce onto the first"); factoryCalls.ShouldBe(1, "the factory must only fire on the create branch"); second.ShouldBeSameAs(first, "both attaches must return the same InFlightRequest reference"); second.InterestedParties.Count.ShouldBe(2, "the second party must be appended in place"); second.InterestedParties[0].OriginalTxId.ShouldBe((ushort)0x1111); second.InterestedParties[1].OriginalTxId.ShouldBe((ushort)0x2222); } finally { await pipeA.DisposeAsync(); await pipeB.DisposeAsync(); } } [Fact] public async Task TryAttachOrCreate_ExistingKey_AtMaxParties_CreatesFreshEntry_NotAppend() { var pipeA = MakePipe(); var pipeB = MakePipe(); var pipeC = MakePipe(); try { var map = new InFlightByKeyMap(); var key = new CoalescingKey(1, 0x03, 100, 1); var partyA = new InterestedParty(pipeA, OriginalTxId: 0xAAAA); var partyB = new InterestedParty(pipeB, OriginalTxId: 0xBBBB); var partyC = new InterestedParty(pipeC, OriginalTxId: 0xCCCC); // MaxParties = 2 — first attach creates, second appends, third overflows. map.TryAttachOrCreate(key, partyA, factory: () => MakeRequest(partyA), maxParties: 2, out var first, out _); map.TryAttachOrCreate(key, partyB, factory: () => MakeRequest(partyB), maxParties: 2, out var second, out _); int factoryCalls = 0; bool ok = map.TryAttachOrCreate(key, partyC, factory: () => { factoryCalls++; return MakeRequest(partyC); }, maxParties: 2, out var third, out bool thirdWasNew); ok.ShouldBeTrue(); thirdWasNew.ShouldBeTrue("the third attach must overflow into a fresh entry"); factoryCalls.ShouldBe(1, "the factory must fire to create the overflow entry"); third.ShouldNotBeSameAs(first, "the overflow must be a distinct InFlightRequest"); third.InterestedParties.Count.ShouldBe(1, "the overflow entry starts with only its triggering party"); first.InterestedParties.Count.ShouldBe(2, "the original entry stays capped at maxParties"); } finally { await pipeA.DisposeAsync(); await pipeB.DisposeAsync(); await pipeC.DisposeAsync(); } } [Fact] public async Task TryRemove_AfterAttach_AllPartiesPresent_InRetrievedEntry() { var pipeA = MakePipe(); var pipeB = MakePipe(); try { var map = new InFlightByKeyMap(); var key = new CoalescingKey(1, 0x03, 100, 1); var partyA = new InterestedParty(pipeA, 1); var partyB = new InterestedParty(pipeB, 2); map.TryAttachOrCreate(key, partyA, () => MakeRequest(partyA), 32, out _, out _); map.TryAttachOrCreate(key, partyB, () => MakeRequest(partyB), 32, out _, out _); bool removed = map.TryRemove(key, out var req); removed.ShouldBeTrue(); req.InterestedParties.Count.ShouldBe(2, "both attached parties must be present in the removed entry"); map.Count.ShouldBe(0); } finally { await pipeA.DisposeAsync(); await pipeB.DisposeAsync(); } } [Fact] public void TryRemove_OfMissing_ReturnsFalse() { var map = new InFlightByKeyMap(); var key = new CoalescingKey(1, 0x03, 100, 1); map.TryRemove(key, out _).ShouldBeFalse("removing a never-attached key must report false"); } [Fact] public async Task Concurrent_AttachOrCreate_From_Two_Threads_NoLostParties_AndNoDuplicateEntries() { // 16 tasks × 500 ops each, all racing on the same key. The map must keep exactly // one entry per key (unlimited MaxParties → no overflow). Each successful attach // must contribute exactly one party to whatever entry was created/joined. // // Each task reuses a single UpstreamPipe across its ops — the map only stores the // InterestedParty reference; pipe state is irrelevant to the map's invariants. // Spinning up 100 × 1000 = 100,000 loopback sockets exhausts the test machine's // ephemeral port pool; we use one pipe per task instead. const int Tasks = 16; const int OpsPerTask = 500; const int MaxParties = int.MaxValue; var map = new InFlightByKeyMap(); var key = new CoalescingKey(1, 0x03, 100, 1); var pipes = new List(Tasks); for (int i = 0; i < Tasks; i++) pipes.Add(MakePipe()); long attaches = 0; long creates = 0; try { var work = new Task[Tasks]; var workCt = TestContext.Current.CancellationToken; for (int t = 0; t < Tasks; t++) { var pipe = pipes[t]; work[t] = Task.Run(() => { for (int i = 0; i < OpsPerTask; i++) { if (workCt.IsCancellationRequested) return; var party = new InterestedParty(pipe, (ushort)i); map.TryAttachOrCreate( key, party, factory: () => MakeRequest(party), maxParties: MaxParties, out _, out bool wasNew); if (wasNew) Interlocked.Increment(ref creates); else Interlocked.Increment(ref attaches); } }, workCt); } await Task.WhenAll(work); (creates + attaches).ShouldBe((long)(Tasks * OpsPerTask), "every op must take exactly one branch"); creates.ShouldBe(1, "all ops share the same key with unlimited MaxParties — exactly one create"); // The retained entry must contain every attached party. bool removed = map.TryRemove(key, out var entry); removed.ShouldBeTrue(); entry.InterestedParties.Count.ShouldBe(Tasks * OpsPerTask, "the entry's party list must hold every attached party — no lost parties under race"); map.Count.ShouldBe(0); } finally { foreach (var p in pipes) try { await p.DisposeAsync(); } catch { } } } }