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
@@ -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