review(Driver.S7): reject writable array tags at init instead of silent write failure

Re-review at 7286d320. S7-015 (Medium): a Writable array tag had no WriteArrayAsync path
and silently returned BadCommunicationError on write; now rejected at init with a clear
NotSupportedException (read-only arrays still accepted) + TDD. S7-016 (factory JSON can't
produce array tags; needs AdminUI DTO) deferred.
This commit is contained in:
Joseph Doherty
2026-06-19 11:34:34 -04:00
parent a914b73d57
commit f2bdd8bc1c
3 changed files with 128 additions and 2 deletions
@@ -225,4 +225,61 @@ public sealed class S7DriverScaffoldTests
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
}
// ── Driver.S7-015 — writable array tag must fail fast at init ────────────────────────
//
// Array reads are implemented (ReadArrayAsync / DecodeArrayBlock), but WriteOneAsync has
// no WriteArrayAsync path. Without the init guard a writable array node is discovered and
// accepted, then every write returns BadCommunicationError (InvalidCastException from
// BoxValueForWrite receiving a typed array). Fail fast at init instead.
/// <summary>Verifies that a writable non-wide array (e.g., Int16[4]) is rejected at init
/// with a clear "array writes not yet supported" message — Driver.S7-015.</summary>
[Theory]
[InlineData(S7DataType.Bool, "DB1.DBX0.0", 4)]
[InlineData(S7DataType.Byte, "DB1.DBB0", 8)]
[InlineData(S7DataType.Int16, "DB1.DBW0", 4)]
[InlineData(S7DataType.UInt16, "DB1.DBW0", 4)]
[InlineData(S7DataType.Int32, "DB1.DBD0", 2)]
[InlineData(S7DataType.UInt32, "DB1.DBD0", 2)]
[InlineData(S7DataType.Float32,"DB1.DBD0", 2)]
public async Task Initialize_rejects_writable_array_tag_with_NotSupportedException(
S7DataType dt, string addr, int count)
{
var opts = new S7DriverOptions
{
Host = "192.0.2.1",
Timeout = TimeSpan.FromMilliseconds(250),
Tags = [new S7TagDefinition("ArrTag", addr, dt, Writable: true, ArrayCount: count)],
};
using var drv = new S7Driver(opts, $"s7-writable-arr-{dt}");
var ex = await Should.ThrowAsync<NotSupportedException>(async () =>
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
ex.Message.ShouldContain("ArrTag");
ex.Message.ShouldContain("array", Case.Insensitive);
ex.Message.ShouldContain("write", Case.Insensitive);
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
}
/// <summary>Verifies that a read-only (Writable=false) array passes the init guard —
/// array reads are fully implemented; only writes are gated.</summary>
[Fact]
public async Task Initialize_accepts_readonly_array_tag()
{
var opts = new S7DriverOptions
{
Host = "192.0.2.1",
Timeout = TimeSpan.FromMilliseconds(250),
Tags = [new S7TagDefinition("ReadArr", "DB1.DBW0", S7DataType.Int16, Writable: false, ArrayCount: 4)],
};
using var drv = new S7Driver(opts, "s7-readonly-arr");
// Must NOT throw NotSupportedException — the failure must be the TCP connect (unreachable host).
var ex = await Should.ThrowAsync<Exception>(async () =>
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
ex.ShouldNotBeOfType<NotSupportedException>(
"read-only arrays are fully supported — the failure must be the TCP connect, not the array guard");
}
}