fix(drivers): flip runtime tag parsers to strict enum parsing (#457)
v2-ci / build (pull_request) Successful in 4m12s
v2-ci / unit-tests (pull_request) Failing after 10m15s

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:
Joseph Doherty
2026-07-15 11:28:34 -04:00
parent 57003a23a5
commit e8db0a683f
16 changed files with 193 additions and 38 deletions
@@ -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);
}
}