fix(drivers): flip runtime tag parsers to strict enum parsing (#457)
R2-11 Phase C. All six equipment-tag parsers (Modbus/S7/AbCip/AbLegacy/ TwinCAT/Focas) now read enum fields via the new TagConfigJson.TryReadEnumStrict: absent -> fallback, valid -> parsed, present-but-invalid (typo) -> TryParse returns false -> EquipmentTagRefResolver.TryResolve false -> driver surfaces BadNodeIdUnknown, instead of the old lenient path that silently defaulted a typo to a wrong-width Good. Modbus flips all three enum fields (region/dataType/byteOrder); the other five flip dataType. The deploy-time Deployment:TagConfigValidationMode=Error gate is unchanged and remains the operator pre-flight. Coverage: - Six *EquipmentTagParserStrictnessTests inverted from Freeze_typo_* (lenient) to Typo_*_rejects_the_tag + Valid_*_still_parses. - TagConfigJsonTests.TryReadEnumStrict_rejects_only_invalid matrix. - Driver-level end-to-end proof: AbCipEquipmentTagTests.Driver_read_of_a_typod_dataType_ref_surfaces_BadNodeIdUnknown drives the real AbCipDriver.ReadAsync through resolve->status. Golden parity corpus has no typo'd enums, so the flip is a no-op there. Full solution builds clean; all six driver suites + core + parity + gate green.
This commit is contained in:
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
/// Pins the shared strict-capable TagConfig field readers (R2-11, 05/CONV-2): the
|
||||
/// <see cref="TagConfigJson.TryReadEnum{TEnum}"/> Absent/Valid/Invalid matrix (case-insensitive),
|
||||
/// <see cref="TagConfigJson.ReadEnumOrDefault{TEnum}"/> lenient equivalence to the retired copies,
|
||||
/// <see cref="TagConfigJson.TryReadEnumStrict{TEnum}"/> reject-only-invalid semantics (R2-11 Phase C),
|
||||
/// <see cref="TagConfigJson.ReadWritable"/> explicit-false-only semantics, and
|
||||
/// <see cref="TagConfigJson.DescribeInvalidEnum{TEnum}"/> messages.
|
||||
/// </summary>
|
||||
@@ -44,6 +45,18 @@ public sealed class TagConfigJsonTests
|
||||
TagConfigJson.ReadEnumOrDefault(Root(json), "k", Sample.Alpha).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"k\":\"Gamma\"}", true, Sample.Gamma)] // valid → parsed, ok
|
||||
[InlineData("{}", true, Sample.Alpha)] // absent → fallback, ok
|
||||
[InlineData("{\"k\":42}", true, Sample.Alpha)] // non-string → Absent → fallback, ok
|
||||
[InlineData("{\"k\":\"typo\"}", false, default(Sample))] // invalid (typo) → REJECT (false)
|
||||
public void TryReadEnumStrict_rejects_only_invalid(string json, bool expectedOk, Sample expectedValue)
|
||||
{
|
||||
var ok = TagConfigJson.TryReadEnumStrict(Root(json), "k", Sample.Alpha, out var v);
|
||||
ok.ShouldBe(expectedOk);
|
||||
v.ShouldBe(expectedValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"writable\":false}", true, false)] // explicit false → false
|
||||
[InlineData("{\"writable\":true}", true, true)] // explicit true → default
|
||||
|
||||
+12
-5
@@ -4,15 +4,22 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>R2-11 (05/CONV-2) strictness surface for the AbCip equipment-tag parser: lenient runtime freeze,
|
||||
/// <c>Inspect</c> reports the typo, and the already-honoured <c>writable</c> key stays bit-identical through
|
||||
/// the shared reader swap.</summary>
|
||||
/// <summary>R2-11 Phase C strictness surface for the AbCip equipment-tag parser: the runtime is now STRICT — a
|
||||
/// typo'd <c>dataType</c> rejects the tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒ <c>BadNodeIdUnknown</c>),
|
||||
/// <c>Inspect</c> reports the typo, and the <c>writable</c> key stays bit-identical through the shared reader.</summary>
|
||||
public sealed class AbCipEquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_DInt()
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DIntt\"}", out var def)
|
||||
AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DIntt\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(AbCipDataType.DInt);
|
||||
}
|
||||
|
||||
@@ -76,4 +76,31 @@ public class AbCipEquipmentTagTests
|
||||
snapshots.Single().StatusCode.ShouldNotBe(AbCipStatusMapper.BadNodeIdUnknown);
|
||||
snapshots.Single().Value.ShouldBe(4242);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end driver-level proof of the R2-11 Phase C flip: an equipment-tag ref carrying a
|
||||
/// TYPO'd <c>dataType</c> enum now REJECTS at the resolver (strict parse ⇒ <c>TryParse</c> false)
|
||||
/// so the read surfaces <see cref="AbCipStatusMapper.BadNodeIdUnknown"/> — instead of the old
|
||||
/// lenient behaviour that silently defaulted to <c>DInt</c> and published a wrong-width
|
||||
/// <c>Good</c>. Same driver + device wiring as the clean-read proof above; only the enum is
|
||||
/// mistyped.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Driver_read_of_a_typod_dataType_ref_surfaces_BadNodeIdUnknown()
|
||||
{
|
||||
var typoJson = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Motor1.Speed","dataType":"DIntt"}""";
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var opts = new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
||||
Tags = [],
|
||||
};
|
||||
var drv = new AbCipDriver(opts, "abcip-eq", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = p => new FakeAbCipTag(p) { Value = 4242 };
|
||||
|
||||
var snapshots = await drv.ReadAsync([typoJson], CancellationToken.None);
|
||||
|
||||
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -4,14 +4,22 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
/// <summary>R2-11 (05/CONV-2) strictness surface for the AbLegacy equipment-tag parser: lenient runtime
|
||||
/// freeze, <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
/// <summary>R2-11 Phase C strictness surface for the AbLegacy equipment-tag parser: the runtime is now STRICT —
|
||||
/// a typo'd <c>dataType</c> rejects the tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒ <c>BadNodeIdUnknown</c>),
|
||||
/// <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
public sealed class AbLegacyEquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_Int()
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}", out var def)
|
||||
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(AbLegacyDataType.Int);
|
||||
}
|
||||
|
||||
+13
-5
@@ -5,16 +5,24 @@ using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// R2-11 (05/CONV-2 + 05/UNDER-1) strictness surface for the FOCAS equipment-tag parser: lenient
|
||||
/// runtime freeze on <c>dataType</c>, forced read-only (<c>Writable == false</c> regardless of any
|
||||
/// authored <c>writable:true</c>), and <c>Inspect</c> reporting the typo + the write-request.
|
||||
/// R2-11 Phase C (+ 05/UNDER-1) strictness surface for the FOCAS equipment-tag parser: the runtime is now
|
||||
/// STRICT — a typo'd <c>dataType</c> rejects the tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒
|
||||
/// <c>BadNodeIdUnknown</c>); FOCAS tags remain forced read-only (<c>Writable == false</c> regardless of any
|
||||
/// authored <c>writable:true</c>), and <c>Inspect</c> reports the typo + the write-request.
|
||||
/// </summary>
|
||||
public sealed class FocasEquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_Int32()
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
FocasEquipmentTagParser.TryParse("{\"address\":\"D0100\",\"dataType\":\"Int322\"}", out var def)
|
||||
FocasEquipmentTagParser.TryParse("{\"address\":\"D0100\",\"dataType\":\"Int322\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
FocasEquipmentTagParser.TryParse("{\"address\":\"D0100\",\"dataType\":\"Int32\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(FocasDataType.Int32);
|
||||
}
|
||||
|
||||
+32
-6
@@ -5,19 +5,45 @@ using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// R2-11 (05/CONV-2) strictness surface for the Modbus equipment-tag parser: the runtime stays lenient
|
||||
/// (freeze), the new <c>Inspect</c> reports the silently-defaulted typo, and the <c>writable</c> key is
|
||||
/// now honoured (default true).
|
||||
/// R2-11 Phase C strictness surface for the Modbus equipment-tag parser: the runtime is now STRICT — a
|
||||
/// present-but-invalid (typo'd) enum on any of the three enum fields (<c>region</c> / <c>dataType</c> /
|
||||
/// <c>byteOrder</c>) rejects the whole tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒
|
||||
/// <c>BadNodeIdUnknown</c>) instead of silently defaulting to a wrong-width <c>Good</c>. <c>Inspect</c>
|
||||
/// still reports the same typo at deploy time, and the <c>writable</c> key is honoured (default true).
|
||||
/// </summary>
|
||||
public sealed class ModbusEquipmentTagParserStrictnessTests
|
||||
{
|
||||
// ---- Phase-A behaviour freeze: a typo'd enum still silently defaults ----
|
||||
// ---- Phase C: a typo'd enum now rejects the tag (BadNodeIdUnknown), not a silent default ----
|
||||
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_Int16()
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
ModbusEquipmentTagParser.TryParse(
|
||||
"{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Intt16\"}", out var def)
|
||||
"{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Intt16\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Typo_region_rejects_the_tag()
|
||||
{
|
||||
ModbusEquipmentTagParser.TryParse(
|
||||
"{\"region\":\"HoldingRegisterz\",\"address\":1,\"dataType\":\"Int16\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Typo_byteOrder_rejects_the_tag()
|
||||
{
|
||||
ModbusEquipmentTagParser.TryParse(
|
||||
"{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Int16\",\"byteOrder\":\"BigEndan\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_enums_still_parse()
|
||||
{
|
||||
ModbusEquipmentTagParser.TryParse(
|
||||
"{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Int16\",\"byteOrder\":\"BigEndian\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(ModbusDataType.Int16);
|
||||
}
|
||||
|
||||
+11
-3
@@ -4,14 +4,22 @@ using ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||
|
||||
/// <summary>R2-11 (05/CONV-2) strictness surface for the S7 equipment-tag parser: lenient runtime freeze,
|
||||
/// <summary>R2-11 Phase C strictness surface for the S7 equipment-tag parser: the runtime is now STRICT — a
|
||||
/// typo'd <c>dataType</c> rejects the tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒ <c>BadNodeIdUnknown</c>),
|
||||
/// <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
public sealed class S7EquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_Int16()
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
S7EquipmentTagParser.TryParse("{\"address\":\"DB1.DBW0\",\"dataType\":\"Intt16\"}", out var def)
|
||||
S7EquipmentTagParser.TryParse("{\"address\":\"DB1.DBW0\",\"dataType\":\"Intt16\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
S7EquipmentTagParser.TryParse("{\"address\":\"DB1.DBW0\",\"dataType\":\"Int16\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(S7DataType.Int16);
|
||||
}
|
||||
|
||||
+12
-4
@@ -4,14 +4,22 @@ using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>R2-11 (05/CONV-2) strictness surface for the TwinCAT equipment-tag parser: lenient runtime
|
||||
/// freeze, <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
/// <summary>R2-11 Phase C strictness surface for the TwinCAT equipment-tag parser: the runtime is now STRICT —
|
||||
/// a typo'd <c>dataType</c> rejects the tag (<c>TryParse</c> ⇒ <see langword="false"/> ⇒ <c>BadNodeIdUnknown</c>),
|
||||
/// <c>Inspect</c> reports the typo, and the <c>writable</c> key is honoured (default true).</summary>
|
||||
public sealed class TwinCATEquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_DInt()
|
||||
public void Typo_dataType_rejects_the_tag()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DIntt\"}", out var def)
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DIntt\"}", out _)
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Valid_dataType_still_parses()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse("{\"symbolPath\":\"MAIN.x\",\"dataType\":\"DInt\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(TwinCATDataType.DInt);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user