mbproxy: strip historical phase/wave/plan references from source comments

Comments described the *history* of how the code arrived (phase numbers,
wave IDs, review IDs, dated TODOs) instead of what it does today. That
scaffolding rotted as the codebase evolved. Cleaned 60 source files +
.gitignore; behaviour unchanged (387/387 tests still pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-14 13:04:30 -04:00
parent b3b8313e9c
commit 1a2856526a
60 changed files with 750 additions and 811 deletions
@@ -49,12 +49,12 @@ public sealed class MultiplexerE2ETests
// ── E2E 1: Five simultaneous upstream clients (connection-cap lift) ──────────────
/// <summary>
/// Headline test for Phase 9: prove that the multiplexer accepts the 5th upstream
/// client on the same proxy port — pre-Phase-9's 1:1 model would have failed at
/// backend connect (H2-ECOM100 cap = 4). Each client's request is serialised behind
/// the previous client's response so the pymodbus 3.13 simulator's concurrent-frame
/// bug never triggers; the multiplexer's connection ceiling, not its under-concurrency
/// behaviour, is what this test proves.
/// Headline test: prove that the multiplexer accepts the 5th upstream client on the
/// same proxy port — a 1:1 model would have failed at backend connect (H2-ECOM100
/// cap = 4). Each client's request is serialised behind the previous client's response
/// so the pymodbus 3.13 simulator's concurrent-frame bug never triggers; the
/// multiplexer's connection ceiling, not its under-concurrency behaviour, is what
/// this test proves.
/// </summary>
[Fact(Timeout = 5_000)]
public async Task E2E_FiveSimultaneousClients_AllReadHR1072_AllGetDecoded_1234()
@@ -82,8 +82,9 @@ public sealed class MultiplexerE2ETests
await using var hd = new AsyncHostDispose(host);
await Task.Delay(200, TestContext.Current.CancellationToken);
// Open five simultaneous TCP connections to the proxy first (each would have used
// a dedicated backend socket pre-Phase-9, blowing through the 4-client cap).
// Open five simultaneous TCP connections to the proxy first (under a 1:1 model
// each would have needed a dedicated backend socket, blowing through the
// 4-client cap).
var clients = new TcpClient[5];
try
{
@@ -380,8 +380,8 @@ public sealed class PlcMultiplexerTests
try
{
// Both clients use the same upstream TxId 0x0007 — the proxy must hand out
// distinct proxy TxIds on the backend wire. Phase 10: reads target DIFFERENT
// addresses so coalescing does not fuse them into a single backend request.
// distinct proxy TxIds on the backend wire. Reads target DIFFERENT addresses
// so coalescing does not fuse them into a single backend request.
await c1.SendAsync(BuildFc03ReadFrame(0x0007, 0, 1), SocketFlags.None);
await c2.SendAsync(BuildFc03ReadFrame(0x0007, 10, 1), SocketFlags.None);
@@ -625,12 +625,12 @@ public sealed class PlcMultiplexerTests
}
}
// ── Phase 12 Wave-1 regression tests ──────────────────────────────────────
// ── ReplaceContext live-swap regression tests ────────────────────────────────
/// <summary>
/// W1.1 — verifies that <see cref="PlcMultiplexer.ReplaceContext"/> swaps the live
/// per-PLC context on the running multiplexer, so the very next PDU's BCD rewriter
/// uses the new tag map (not the captured-at-construction map). Before W1.1 this
/// Verifies that <see cref="PlcMultiplexer.ReplaceContext"/> swaps the live per-PLC
/// context on the running multiplexer, so the very next PDU's BCD rewriter uses the
/// new tag map (not the captured-at-construction map). Without the live swap this
/// scenario would silently keep using the old map until the listener faulted and the
/// supervisor's Polly loop reconstructed everything.
/// </summary>
@@ -682,11 +682,11 @@ public sealed class PlcMultiplexerTests
}
/// <summary>
/// W1.1 — verifies that swapping in a fresh response cache via <see cref="PlcMultiplexer.ReplaceContext"/>
/// makes the running multiplexer consult the NEW cache for subsequent reads, not the
/// old cache that was disposed by the supervisor. Without W1.1 the running mux would
/// keep its constructor-captured cache reference and either return stale entries or
/// hit a disposed cache.
/// Verifies that swapping in a fresh response cache via
/// <see cref="PlcMultiplexer.ReplaceContext"/> makes the running multiplexer consult
/// the NEW cache for subsequent reads, not the old cache that was disposed by the
/// supervisor. Without the live swap the running mux would keep its constructor-
/// captured cache reference and either return stale entries or hit a disposed cache.
/// </summary>
[Fact]
public async Task ReplaceContext_NewCache_NextReadGoesToBackend_NotOldCache()
@@ -757,7 +757,7 @@ public sealed class PlcMultiplexerTests
}
}
// ── Phase 12 (W3 final-tier race tests) ──────────────────────────────────
// ── Final-tier race tests ─────────────────────────────────────────────────
/// <summary>
/// Reflection helper — drains the multiplexer's TxIdAllocator by calling
@@ -781,10 +781,10 @@ public sealed class PlcMultiplexerTests
}
/// <summary>
/// W3 #5 — TxId allocator saturation propagates as a Modbus exception 04 to the
/// upstream client (no hang, no crash). The 16-bit TxId space (65,536 slots) is
/// pre-saturated via reflection so the next request hits the
/// <c>!_allocator.TryAllocate</c> branch in <c>OnUpstreamFrameAsync</c> immediately.
/// TxId allocator saturation propagates as a Modbus exception 04 to the upstream
/// client (no hang, no crash). The 16-bit TxId space (65,536 slots) is pre-saturated
/// via reflection so the next request hits the <c>!_allocator.TryAllocate</c> branch
/// in <c>OnUpstreamFrameAsync</c> immediately.
/// </summary>
[Fact]
public async Task TxIdAllocator_Saturated_NextRequest_GetsException04_WithOriginalTxId()
@@ -835,11 +835,11 @@ public sealed class PlcMultiplexerTests
}
/// <summary>
/// W3 #6 — under TxId saturation, two concurrent identical FC03 reads must BOTH
/// receive exception 04 (one as the leader directly, the other either via a
/// coalesced fan-out from the W1.2 cleanup OR via its own independent saturation
/// path — either timing produces the same observable contract). Validates that no
/// pipe hangs forever waiting for a backend response that would never arrive.
/// Under TxId saturation, two concurrent identical FC03 reads must BOTH receive
/// exception 04 (one as the leader directly, the other either via a coalesced
/// fan-out from the saturation cleanup OR via its own independent saturation path —
/// either timing produces the same observable contract). Validates that no pipe
/// hangs forever waiting for a backend response that would never arrive.
/// </summary>
[Fact]
public async Task TxIdAllocator_Saturated_TwoConcurrentIdenticalReads_BothPipesGetException04()
@@ -877,7 +877,7 @@ public sealed class PlcMultiplexerTests
var rspA = await ReadOneFrameAsync(cA, TestContext.Current.CancellationToken);
var rspB = await ReadOneFrameAsync(cB, TestContext.Current.CancellationToken);
// Both must be exception 04 with the original TxId echoed — the W1.2 contract
// Both must be exception 04 with the original TxId echoed — the contract
// is "no late attacher hangs."
foreach (var (rsp, expectedTxId, label) in new[] {
(rspA, txA, "A"), (rspB, txB, "B") })
@@ -898,14 +898,14 @@ public sealed class PlcMultiplexerTests
}
/// <summary>
/// W3 #7 — backend-reader head-of-line block. One upstream pipe is wedged by the
/// test holding its socket-receive side without reading. The W1.3 fix routes the
/// fan-out through <c>TrySendResponse</c> so the per-PLC backend reader cannot be
/// stalled by a wedged pipe; responses to a healthy peer must keep flowing and the
/// wedged pipe's <c>responseDropForFullUpstream</c> counter must increment.
/// Backend-reader head-of-line block guard. One upstream pipe is wedged by the test
/// holding its socket-receive side without reading. The fan-out is routed through
/// <c>TrySendResponse</c> so the per-PLC backend reader cannot be stalled by a
/// wedged pipe; responses to a healthy peer must keep flowing and the wedged pipe's
/// <c>responseDropForFullUpstream</c> counter must increment.
///
/// <para>Pre-W1.3 the synchronous <c>await SendResponseAsync</c> inside the reader
/// would block on the wedged pipe's full bounded channel and starve every peer.</para>
/// <para>A synchronous <c>await SendResponseAsync</c> inside the reader would block
/// on the wedged pipe's full bounded channel and starve every peer.</para>
/// </summary>
[Fact]
public async Task SlowUpstream_DoesNotStallPeerResponses_DropCounterIncrements()
@@ -943,8 +943,8 @@ public sealed class PlcMultiplexerTests
await cB.SendAsync(BuildFc03ReadFrame(txB, 0, 1), SocketFlags.None);
// B's response must arrive within a few hundred ms even with A wedged. If
// the W1.3 fix were missing, the reader would be blocked on A's channel and
// B would time out.
// the non-blocking enqueue path were missing, the reader would be blocked on
// A's channel and B would time out.
var rspB = await ReadOneFrameAsync(cB, TestContext.Current.CancellationToken)
.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
ushort echoB = (ushort)((rspB[0] << 8) | rspB[1]);
@@ -972,7 +972,7 @@ public sealed class PlcMultiplexerTests
}
/// <summary>
/// W3 #8 — watchdog↔response race. The W1 design uses claim-then-dispatch:
/// Watchdog↔response race. The design uses claim-then-dispatch:
/// <c>CorrelationMap.TryRemove</c> is the single source of truth, so exactly ONE
/// of (response delivered, watchdog timeout) wins for any given proxy TxId. This
/// test exercises the race window directly: a stub backend that responds at almost
@@ -992,13 +992,13 @@ public sealed class PlcMultiplexerTests
// = 400 the tick is 100 ms. Configure the backend to delay 350-450 ms for each
// request so some land before, some after the timeout.
int backendPort = PickFreePort();
// Phase 12 (W4 / T2) — deterministic alternation rather than seeded Random. Random
// with a fixed seed is not stable across .NET major versions (Microsoft has changed
// the implementation, e.g. legacy → Xoshiro128 in .NET 6), so a runtime upgrade
// could land all samples on one side of the watchdog deadline and break the
// "both branches must fire" assertion below. Counter-based alternation guarantees
// 15 fast (350 ms, beats watchdog) and 15 slow (450 ms, loses to watchdog) responses
// across 30 iterations, regardless of runtime.
// Deterministic alternation rather than seeded Random. Random with a fixed seed is
// not stable across .NET major versions (Microsoft has changed the implementation,
// e.g. legacy → Xoshiro128 in .NET 6), so a runtime upgrade could land all samples
// on one side of the watchdog deadline and break the "both branches must fire"
// assertion below. Counter-based alternation guarantees 15 fast (350 ms, beats
// watchdog) and 15 slow (450 ms, loses to watchdog) responses across 30 iterations,
// regardless of runtime.
int reqCount = 0;
var slowBackend = new SlowResponseBackend(backendPort, () =>
{
@@ -1062,13 +1062,13 @@ public sealed class PlcMultiplexerTests
}
/// <summary>
/// W3 #9 — cascade racing with new accepts. Stress-test: while the backend is repeatedly
/// Cascade racing with new accepts. Stress-test: while the backend is repeatedly
/// killed and resurrected (forcing repeated cascade cycles), new upstream clients
/// connect and disconnect concurrently. The contract verified is the
/// no-crash-under-churn property: the multiplexer must survive arbitrary interleavings
/// of teardown and new-pipe-attach without throwing into the host or leaking sockets.
///
/// <para>The originally-flagged race window — a new pipe added between
/// <para>The race window — a new pipe added between
/// <c>_pipes.Values.ToArray()</c> and <c>_pipes.Clear()</c> in <c>TearDownBackendAsync</c>
/// — leaves the new pipe alive but orphaned from <c>_pipes</c>. Its read loop will
/// receive normal traffic until the next cascade or its socket closes. This test
@@ -1165,7 +1165,7 @@ public sealed class PlcMultiplexerTests
/// <summary>
/// Backend stub that delays each response by a caller-supplied amount. Used by the
/// W3 #8 watchdog race test.
/// watchdog-vs-response race test.
/// </summary>
private sealed class SlowResponseBackend : IAsyncDisposable
{
@@ -10,9 +10,9 @@ namespace Mbproxy.Tests.Proxy.Multiplexing;
/// <summary>
/// Verifies that <see cref="BcdPduPipeline"/> correlates FC03/FC04 responses through
/// <see cref="PerPlcContext.CurrentRequest"/> (Phase 9) rather than the pre-Phase-9
/// per-pair last-request slot. Concurrent in-flight requests from different upstream
/// clients must decode against their own request range without cross-talk.
/// <see cref="PerPlcContext.CurrentRequest"/>. Concurrent in-flight requests from
/// different upstream clients must decode against their own request range without
/// cross-talk.
/// </summary>
[Trait("Category", "Unit")]
public sealed class RewriterCorrelationTests
@@ -9,8 +9,8 @@ namespace Mbproxy.Tests.Proxy.Multiplexing;
/// <summary>
/// Unit tests for <see cref="UpstreamPipe"/>'s response-channel contract — particularly
/// the Phase 12 (W1.3) <see cref="UpstreamPipe.TrySendResponse"/> non-blocking enqueue
/// added so the per-PLC backend reader cannot be stalled by one slow upstream client.
/// the <see cref="UpstreamPipe.TrySendResponse"/> non-blocking enqueue, which exists
/// so the per-PLC backend reader cannot be stalled by one slow upstream client.
/// </summary>
[Trait("Category", "Unit")]
public sealed class UpstreamPipeTests
@@ -40,7 +40,7 @@ public sealed class UpstreamPipeTests
// ── Tests ─────────────────────────────────────────────────────────────────
/// <summary>
/// W1.3 — when no write-loop is draining the response channel, repeated
/// When no write-loop is draining the response channel, repeated
/// <see cref="UpstreamPipe.TrySendResponse"/> calls must succeed up to the channel's
/// bounded capacity and return <c>false</c> on every subsequent call without blocking.
/// This is the non-blocking contract the per-PLC backend reader relies on.
@@ -80,7 +80,7 @@ public sealed class UpstreamPipeTests
}
/// <summary>
/// W1.3 — once the pipe has been disposed, <see cref="UpstreamPipe.TrySendResponse"/>
/// Once the pipe has been disposed, <see cref="UpstreamPipe.TrySendResponse"/>
/// returns <c>false</c> regardless of channel state, never throws.
/// </summary>
[Fact]