fix(s7): refuse unservable tag shapes per tag at every gate (R07:H3, R07:U4)
The init-time tag guards THREW: one mis-authored tag (DB1.DBZ0, DB1.DBW0 typed Float64, T5 typed Int32, a writable Bool array) faulted the whole driver and the actor retried the same init every 10 s forever, darkening every tag on the PLC. Neither the editor nor the deploy gate checked any of it. - S7AddressParser moves to S7.Contracts (same namespace) and a new S7TagShapeRules there is the single authority on servable shapes. - S7TagDefinitionFactory.FromTagConfig applies it, so the AdminUI editor, the deploy gate (Inspect, with the specific reason) and the driver init refuse the same tags. Init now skips a refused tag (ISkippedTagReporter) instead of throwing; the throwing RejectUnsupportedTagConfigs is gone. - New rules on top of the old guards: a narrow type at the wrong access width (every read failed) and a Bool array not starting at bit 0 (U4: DB1.DBX0.3 silently read bit 0 of bytes 0..n). - AdminUI test fixtures that used addresses the driver could never serve (DB1.DBS0, a browse "SomeAddress", Int32/Float32 on DBW) now use real ones. Refs #742 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -25,26 +25,10 @@ public sealed class S7DriverCodeReviewFixTests
|
||||
[InlineData("C10")]
|
||||
public async Task Initialize_rejects_timer_or_counter_tag_with_NotSupportedException(string address)
|
||||
{
|
||||
// A Timer/Counter address parses cleanly but the read path has no decode case for it,
|
||||
// so it must fail fast at init rather than throw a misleading type-mismatch on every
|
||||
// read. The host is reserved-for-documentation so the TCP connect can never succeed —
|
||||
// the unsupported-address guard runs before the connect, so the NotSupportedException
|
||||
// is what surfaces.
|
||||
var opts = new S7DriverOptions
|
||||
{
|
||||
Host = "192.0.2.1",
|
||||
Timeout = TimeSpan.FromMilliseconds(250),
|
||||
RawTags = S7RawTags.Entries(new S7TagDefinition("Quirk", address, S7DataType.Int16)),
|
||||
};
|
||||
using var drv = new S7Driver(opts, "s7-tc");
|
||||
|
||||
var ex = await Should.ThrowAsync<NotSupportedException>(async () =>
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
||||
ex.Message.ShouldContain(address);
|
||||
|
||||
var health = drv.GetHealth();
|
||||
health.State.ShouldBe(DriverState.Faulted, "an unsupported-address config error must Fault the driver");
|
||||
health.LastError.ShouldNotBeNull();
|
||||
// A Timer/Counter address typed anything but Float64 (Timer) / Int32 (Counter) cannot be decoded.
|
||||
// It is refused PER TAG at every gate; it used to throw at init and fault every tag on the PLC.
|
||||
await S7TagSkipAssert.RefusedPerTagAsync(
|
||||
new S7TagDefinition("Quirk", address, S7DataType.Int16, Writable: false), address);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that initialize accepts DB and MIQ addresses without triggering the unsupported guard.</summary>
|
||||
@@ -102,19 +86,21 @@ public sealed class S7DriverCodeReviewFixTests
|
||||
[Fact]
|
||||
public async Task Initialize_rejects_a_timer_tag_supplied_only_through_driverConfigJson()
|
||||
{
|
||||
// The Timer/Counter guard must run against the *re-parsed* config, not just the
|
||||
// constructor options — proves Driver.S7-001 and Driver.S7-011 compose correctly.
|
||||
// The shape gate must run against the *re-parsed* config, not just the constructor options —
|
||||
// proves the per-tag skip and Driver.S7-011 compose correctly.
|
||||
var ctorOpts = new S7DriverOptions { Host = "192.0.2.1", Timeout = TimeSpan.FromMilliseconds(250) };
|
||||
using var drv = new S7Driver(ctorOpts, "s7-cfg-tc");
|
||||
using var drv = new S7Driver(ctorOpts, "s7-cfg-tc", new ScriptedS7PlcFactory());
|
||||
|
||||
const string json = """
|
||||
{ "Host": "192.0.2.1", "TimeoutMs": 250,
|
||||
{ "Host": "192.0.2.1", "TimeoutMs": 250, "Probe": { "Enabled": false },
|
||||
"RawTags": [ { "RawPath": "TimerTag", "TagConfig": "{\"address\":\"T5\",\"dataType\":\"Int16\"}", "WriteIdempotent": false } ] }
|
||||
""";
|
||||
|
||||
var ex = await Should.ThrowAsync<NotSupportedException>(async () =>
|
||||
await drv.InitializeAsync(json, TestContext.Current.CancellationToken));
|
||||
ex.Message.ShouldContain("T5");
|
||||
await drv.InitializeAsync(json, TestContext.Current.CancellationToken);
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
var skipped = drv.GetSkippedTags().ShouldNotBeNull().Sample.ShouldHaveSingleItem();
|
||||
skipped.RawPath.ShouldBe("TimerTag");
|
||||
skipped.Reason.ShouldContain("T5");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reinitialize applies a changed driverConfigJson.</summary>
|
||||
|
||||
Reference in New Issue
Block a user