fix(s7): classic STRING/DT unrepresentable writes map to BadOutOfRange, not BadCommunicationError (#632)

S7.Net's own STRING/DT encoders throw ArgumentException; that fell into the
generic write catch (BadCommunicationError + Degraded health). Now surfaced as
S7ValueRangeException like the WSTRING/DATE codecs.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 16:51:20 -04:00
parent a8b07ae12d
commit 41e600e069
3 changed files with 40 additions and 4 deletions
@@ -299,7 +299,7 @@ public sealed class S7ScalarBlockTests
{
var tag = Tag(S7DataType.String, stringLength: 5);
// 6 chars into a 5-char reserved field — S7.Net throws ArgumentException.
Should.Throw<ArgumentException>(() => S7Driver.EncodeScalarBlock(tag, "ABCDEF"));
Should.Throw<S7ValueRangeException>(() => S7Driver.EncodeScalarBlock(tag, "ABCDEF"));
}
// ── String round-trip identity (encode → decode) ──────────────────────────────────────
@@ -376,7 +376,7 @@ public sealed class S7ScalarBlockTests
{
var tag = Tag(S7DataType.DateTime);
var value = new System.DateTime(year, 1, 1, 0, 0, 0);
Should.Throw<ArgumentOutOfRangeException>(() => S7Driver.EncodeScalarBlock(tag, value));
Should.Throw<S7ValueRangeException>(() => S7Driver.EncodeScalarBlock(tag, value));
}
// ── DateTime round-trip identity (encode → decode) ────────────────────────────────────
@@ -407,6 +407,35 @@ public sealed class S7ValueCodecTests
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
}
/// <summary>
/// #632 live-coverage finding: classic STRING and DT writes use S7.Net's own encoders, which throw
/// <see cref="ArgumentException"/> / <see cref="ArgumentOutOfRangeException"/> for an unrepresentable
/// value. That fell into the generic catch (BadCommunicationError + Degraded health) while the
/// sibling WSTRING/DATE codecs return BadOutOfRange — a client error must not look like a comms fault.
/// </summary>
[Fact]
public async Task Classic_STRING_and_DT_unrepresentable_writes_are_BadOutOfRange_without_degrading_health()
{
var plc = new MemoryS7Plc();
var opts = new S7DriverOptions
{
Host = "192.0.2.1",
Probe = new S7ProbeOptions { Enabled = false },
RawTags = S7RawTags.Entries(
new S7TagDefinition("Str", "DB1.DBB0", S7DataType.String, StringLength: 4),
new S7TagDefinition("Dt", "DB1.DBB20", S7DataType.DateTime)),
};
using var drv = new S7Driver(opts, "s7-classic-range", new MemoryS7PlcFactory(plc));
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var ct = TestContext.Current.CancellationToken;
(await drv.WriteAsync([new WriteRequest("Str", "abcd")], ct))[0].StatusCode.ShouldBe(0u);
(await drv.WriteAsync([new WriteRequest("Str", "abcde")], ct))[0].StatusCode.ShouldBe(0x803C0000u);
(await drv.WriteAsync([new WriteRequest("Dt", new DateTime(1980, 1, 1))], ct))[0].StatusCode.ShouldBe(0x803C0000u);
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
(await drv.ReadAsync(["Str"], ct))[0].Value.ShouldBe("abcd");
}
// ---- fakes ----
private sealed class MemoryS7PlcFactory(MemoryS7Plc plc) : IS7PlcFactory