fix(r2-01): RED framing-fault classification Theory T5 (STAB-15a, task 6)

This commit is contained in:
Joseph Doherty
2026-07-13 09:57:08 -04:00
parent 066514fed3
commit 8aed9ac365
2 changed files with 45 additions and 1 deletions
@@ -40,7 +40,7 @@
{
"id": 6,
"subject": "RED: framing-fault classification Theory T5 (TPKT/TPDU/WrongNumberOfBytes/PlcException-WrongNumberReceivedBytes reopen) — must FAIL",
"status": "pending",
"status": "completed",
"blockedBy": [0]
},
{
@@ -247,6 +247,50 @@ public sealed class S7DriverReconnectTests
async () => await drv.ReadAsync(["W0"], cts.Token));
}
// ---- STAB-15a: ISO-on-TCP framing/desync faults must reopen ----
/// <summary>
/// T5 (STAB-15a). A framing/desync fault on ISO-on-TCP (S7.Net's
/// <see cref="TPKTInvalidException"/> / <see cref="TPDUInvalidException"/> /
/// <see cref="WrongNumberOfBytesException"/>, or a <see cref="PlcException"/> carrying
/// <see cref="ErrorCode.WrongNumberReceivedBytes"/>) leaves the stream position
/// untrustworthy — only a reopen recovers. The next read must dispose connection #1 and
/// open a fresh #2. Before the fix these were not classified fatal, so the desynced handle
/// was reused forever (<c>Created.Count</c> pinned at 1).
/// </summary>
[Theory]
[InlineData("tpkt")]
[InlineData("tpdu")]
[InlineData("wrongbytes")]
[InlineData("plc-wrongnumber")]
public async Task Framing_faults_are_classified_connection_fatal_and_reopen(string kind)
{
Exception framing = kind switch
{
"tpkt" => new TPKTInvalidException(),
"tpdu" => new TPDUInvalidException(),
"wrongbytes" => new WrongNumberOfBytesException(),
"plc-wrongnumber" => new PlcException(ErrorCode.WrongNumberReceivedBytes, "short read"),
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
var factory = new FakeS7PlcFactory();
using var drv = new S7Driver(Options(), "s7-framing", factory);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
// Read #1 hits the framing fault — Bad status, handle marked dead, but no reopen yet.
factory.Created[0].NextReadThrows = framing;
var bad = await drv.ReadAsync(["W0"], TestContext.Current.CancellationToken);
bad[0].StatusCode.ShouldNotBe(0u);
factory.Created.Count.ShouldBe(1);
// Read #2 reopens a fresh connection and reads Good.
var recovered = await drv.ReadAsync(["W0"], TestContext.Current.CancellationToken);
recovered[0].StatusCode.ShouldBe(0u);
factory.Created.Count.ShouldBe(2);
factory.Created[0].Disposed.ShouldBeTrue();
}
// ---- fakes ----
private sealed class FakeS7PlcFactory : IS7PlcFactory