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:
@@ -142,7 +142,7 @@ public sealed class CsvMergeEvidenceTests
|
||||
var row = SingleRow(
|
||||
DriverTypeNames.S7,
|
||||
"Name,DataType,Address,S7DataType,TagConfigJson",
|
||||
"t1,Int16,DB1.DBW0,Int32,\"{\"\"pollHint\"\":\"\"fast\"\"}\"");
|
||||
"t1,Int16,DB1.DBD0,Int32,\"{\"\"pollHint\"\":\"\"fast\"\"}\"");
|
||||
|
||||
var blob = Blob(row);
|
||||
blob["dataType"]!.GetValue<string>().ShouldBe("Int32");
|
||||
|
||||
@@ -346,7 +346,11 @@ public sealed class RawTagConfigGateTests
|
||||
public void A_browse_committed_config_passes_the_gate(string driverType)
|
||||
{
|
||||
var tagConfig = RawBrowseCommitMapper.BuildTagConfig(
|
||||
driverType, driverType == DriverTypeNames.AbLegacy ? "N7:0" : "SomeAddress", BrowseFieldsFor(driverType));
|
||||
driverType,
|
||||
// Drivers whose address grammar is validated by their reader need a real address: AbLegacy's
|
||||
// file grammar, and S7's (S7TagShapeRules — an unparseable or wrong-width address is refused).
|
||||
driverType == DriverTypeNames.AbLegacy ? "N7:0" : driverType == DriverTypeNames.S7 ? "DB1.DBW0" : "SomeAddress",
|
||||
BrowseFieldsFor(driverType));
|
||||
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldBeNull(
|
||||
$"browse-commit for {driverType} must not author a config the import refuses");
|
||||
|
||||
@@ -98,13 +98,13 @@ public sealed class S7TagConfigModelTests
|
||||
/// driver serves. (An absent key is deliberately not asserted: S7 alone hard-requires the key, an
|
||||
/// asymmetry that predates this change and that its own <c>Inspect</c> does not report either.)</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":"float32"}""")]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":"FLOAT32"}""")]
|
||||
[InlineData("""{"address":"DB1.DBD0","dataType":"float32"}""")]
|
||||
[InlineData("""{"address":"DB1.DBD0","dataType":"FLOAT32"}""")]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":7}""")]
|
||||
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
|
||||
=> S7TagConfigModel.FromJson(json).Validate().ShouldBeNull();
|
||||
|
||||
// ---- S7-016: array shape vs the driver's init guards (which fail the WHOLE driver init) ----
|
||||
// ---- S7-016: array shape vs S7TagShapeRules (refused per tag at every gate) ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("Int64")]
|
||||
|
||||
@@ -160,9 +160,9 @@ public sealed class TagConfigNonEnumParityTests
|
||||
/// <summary>S7's remaining non-enum refusal: a <c>String</c> tag whose <c>stringLength</c> falls outside
|
||||
/// 0–254. The reader REJECTS it (it does not clamp), so the tag would never resolve.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"address":"DB1.DBS0","dataType":"String","stringLength":255}""")]
|
||||
[InlineData("""{"address":"DB1.DBS0","dataType":"String","stringLength":300}""")]
|
||||
[InlineData("""{"address":"DB1.DBS0","dataType":"String","stringLength":-1}""")]
|
||||
[InlineData("""{"address":"DB1.DBB0","dataType":"String","stringLength":255}""")]
|
||||
[InlineData("""{"address":"DB1.DBB0","dataType":"String","stringLength":300}""")]
|
||||
[InlineData("""{"address":"DB1.DBB0","dataType":"String","stringLength":-1}""")]
|
||||
public void S7_refuses_a_string_length_its_reader_refuses(string blob)
|
||||
{
|
||||
RuntimeAccepts(DriverTypeNames.S7, blob).ShouldBeFalse(
|
||||
@@ -174,9 +174,9 @@ public sealed class TagConfigNonEnumParityTests
|
||||
/// stops this becoming an over-rejection — an out-of-range <c>stringLength</c> on a tag that is not a
|
||||
/// String at all, which the reader ignores entirely.</summary>
|
||||
[Theory]
|
||||
[InlineData("""{"address":"DB1.DBS0","dataType":"String","stringLength":10}""")]
|
||||
[InlineData("""{"address":"DB1.DBS0","dataType":"String","stringLength":254}""")]
|
||||
[InlineData("""{"address":"DB1.DBS0","dataType":"String","stringLength":0}""")]
|
||||
[InlineData("""{"address":"DB1.DBB0","dataType":"String","stringLength":10}""")]
|
||||
[InlineData("""{"address":"DB1.DBB0","dataType":"String","stringLength":254}""")]
|
||||
[InlineData("""{"address":"DB1.DBB0","dataType":"String","stringLength":0}""")]
|
||||
[InlineData("""{"address":"DB1.DBW0","dataType":"Int16","stringLength":300}""")]
|
||||
public void S7_accepts_a_string_length_its_reader_accepts(string blob)
|
||||
{
|
||||
@@ -360,8 +360,8 @@ public sealed class TagConfigNonEnumParityTests
|
||||
(DriverTypeNames.S7, """{"address":"DB1.DBW0","dataType":"Int16"}"""),
|
||||
(DriverTypeNames.S7, """{"address":"DB1.DBW0"}"""),
|
||||
(DriverTypeNames.S7, """{"address":" ","dataType":"Int16"}"""),
|
||||
(DriverTypeNames.S7, """{"address":"DB1.DBS0","dataType":"String","stringLength":300}"""),
|
||||
(DriverTypeNames.S7, """{"address":"DB1.DBS0","dataType":"String","stringLength":24}"""),
|
||||
(DriverTypeNames.S7, """{"address":"DB1.DBB0","dataType":"String","stringLength":300}"""),
|
||||
(DriverTypeNames.S7, """{"address":"DB1.DBB0","dataType":"String","stringLength":24}"""),
|
||||
|
||||
(DriverTypeNames.AbCip, "{}"),
|
||||
(DriverTypeNames.AbCip, """{"tagPath":"Program:Main.Speed"}"""),
|
||||
@@ -576,7 +576,7 @@ public sealed class TagConfigNonEnumParityTests
|
||||
data.Add(DriverTypeNames.Modbus, new ModbusTagConfigModel { Address = 1 }.ToJson());
|
||||
data.Add(DriverTypeNames.S7, new S7TagConfigModel { Address = "DB1.DBW0" }.ToJson());
|
||||
data.Add(DriverTypeNames.S7,
|
||||
new S7TagConfigModel { Address = "DB1.DBS0", DataType = S7DataType.String, StringLength = 24 }.ToJson());
|
||||
new S7TagConfigModel { Address = "DB1.DBB0", DataType = S7DataType.String, StringLength = 24 }.ToJson());
|
||||
data.Add(DriverTypeNames.AbCip, new AbCipTagConfigModel { TagPath = "Program:Main.Speed" }.ToJson());
|
||||
data.Add(DriverTypeNames.AbLegacy, new AbLegacyTagConfigModel { Address = "N7:0" }.ToJson());
|
||||
data.Add(DriverTypeNames.TwinCAT, new TwinCATTagConfigModel { SymbolPath = "MAIN.bStart" }.ToJson());
|
||||
|
||||
Reference in New Issue
Block a user