feat(s7): byte-buffer codec dispatch + Int64/UInt64/LReal scalar read+write

This commit is contained in:
Joseph Doherty
2026-06-17 05:38:18 -04:00
parent 06b858eb02
commit 286be5df88
7 changed files with 453 additions and 128 deletions
@@ -7,8 +7,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
/// <summary>
/// Regression tests for the remaining code-review findings closed against the S7 driver:
/// Driver.S7-003 (Read/WriteAsync null-arg validation), Driver.S7-009 (poll-loop health
/// update + backoff), Driver.S7-010 (Dispose without sync-over-async), and Driver.S7-013
/// (reject not-yet-implemented S7DataType values at init).
/// update + backoff), Driver.S7-010 (Dispose without sync-over-async), and the Phase 4d
/// guard-(b) (wide/structured types must be byte-addressed). The wide types are no longer
/// rejected wholesale at init — the 8-byte numerics round-trip through the byte-buffer codec
/// (see <see cref="S7ScalarBlockTests"/>); only a wide type at a non-byte address is rejected.
/// </summary>
[Trait("Category", "Unit")]
public sealed class S7DriverCodeReviewFixTests2
@@ -136,31 +138,36 @@ public sealed class S7DriverCodeReviewFixTests2
Should.NotThrow(() => drv.Dispose());
}
// ── Driver.S7-013 — Reject not-yet-implemented S7DataType values at init ─────────────
// ── Phase 4d guard-(b) — wide/structured types must be byte-addressed ────────────────
/// <summary>Verifies that Initialize rejects not-yet-implemented data types with NotSupportedException.</summary>
/// <param name="dt">The S7 data type that is not yet implemented.</param>
/// <summary>
/// Verifies the init guard rejects a wide/structured type authored at a non-byte address
/// (here <c>DB1.DBD0</c>, a DWord). Phase 4d wired the 8-byte numerics (Int64/UInt64/
/// Float64) through the byte-buffer codec — see <see cref="S7ScalarBlockTests"/> for the
/// round-trip coverage — but they (and the still-deferred String/DateTime) decode from a
/// byte-anchored block (<c>DBB</c>/<c>MB</c>/<c>IB</c>/<c>QB</c>). A non-byte suffix would
/// mis-frame the value, so <c>RejectUnsupportedTagConfigs</c> guard-(b) fails the config
/// fast at init rather than as a misleading per-read fault.
/// </summary>
/// <param name="dt">A wide/structured S7 data type authored at a non-byte address.</param>
[Theory]
[InlineData(S7DataType.Int64)]
[InlineData(S7DataType.UInt64)]
[InlineData(S7DataType.Float64)]
[InlineData(S7DataType.String)]
[InlineData(S7DataType.DateTime)]
public async Task Initialize_rejects_not_yet_implemented_data_type_with_NotSupportedException(S7DataType dt)
public async Task Initialize_rejects_wide_type_at_non_byte_address_with_NotSupportedException(S7DataType dt)
{
// A tag declared with one of the not-yet-wired data types parses cleanly and creates
// an OPC UA node via DiscoverAsync — then every Read/Write of it returns BadNotSupported.
// The half-implemented type must be rejected at init so a site can't deploy a config
// that produces dead nodes (Driver.S7-013).
var opts = new S7DriverOptions
{
Host = "192.0.2.1",
Timeout = TimeSpan.FromMilliseconds(250),
// Use a DB.DBD address — the parser accepts it for every data type. The init guard
// must fault on the data-type rather than on the address.
// DB1.DBD0 is a DWord (non-byte) address — guard-(b) must fault on the address shape
// for a wide type. (At a byte address like DB1.DBB0 the 8-byte numerics round-trip;
// see S7ScalarBlockTests.)
Tags = [new S7TagDefinition("X", "DB1.DBD0", dt)],
};
using var drv = new S7Driver(opts, $"s7-bad-dt-{dt}");
using var drv = new S7Driver(opts, $"s7-wide-nonbyte-{dt}");
var ex = await Should.ThrowAsync<NotSupportedException>(async () =>
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));