From 8a7701db3f15e5a15f1fc7585a131a9af27f6a54 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 12:23:43 -0400 Subject: [PATCH] test(drivers): failing repro for CONV-4 equipment-tag ResolveHost mis-keying --- ...2-09-driver-fleet-batch-plan.md.tasks.json | 2 +- .../AbCipResolveHostTests.cs | 43 +++++++++++++++ .../AbLegacyResolveHostTests.cs | 42 +++++++++++++++ .../FocasResolveHostTests.cs | 52 +++++++++++++++++++ .../TwinCATResolveHostTests.cs | 43 +++++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasResolveHostTests.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATResolveHostTests.cs diff --git a/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json b/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json index b154d0d3..d3016198 100644 --- a/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json +++ b/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json @@ -155,7 +155,7 @@ { "id": "B5.1", "subject": "B5: failing ResolveHost tests in AbCip/AbLegacy/TwinCAT/FOCAS (equipment ref for device B must key B; fallback regression pins) \u2014 CONV-4 repro", - "status": "pending", + "status": "completed", "blockedBy": [] }, { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs new file mode 100644 index 00000000..fd305b8f --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs @@ -0,0 +1,43 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.AbCip; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; + +/// +/// CONV-4 — keys per-host resilience (bulkhead / circuit +/// breaker). It must resolve an equipment-tag reference (raw TagConfig JSON) to its OWN device +/// host, not fall back to the first device — otherwise a broken device B trips device A's +/// breaker. Authored tags and unknown refs keep the current fallback. +/// +[Trait("Category", "Unit")] +public sealed class AbCipResolveHostTests +{ + private const string DeviceA = "ab://10.0.0.5/1,0"; + private const string DeviceB = "ab://10.0.0.6/1,0"; + + private static AbCipDriver NewDriver() => new( + new AbCipDriverOptions + { + Devices = [new AbCipDeviceOptions(DeviceA), new AbCipDeviceOptions(DeviceB)], + Probe = new AbCipProbeOptions { Enabled = false }, + }, + "abcip-1", new FakeAbCipTagFactory()); + + /// An equipment-tag ref naming device B resolves to device B's host, not the first device. + [Fact] + public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost() + { + var drv = NewDriver(); + var json = $$"""{"deviceHostAddress":"{{DeviceB}}","tagPath":"Motor1.Speed","dataType":"DInt"}"""; + drv.ResolveHost(json).ShouldBe(DeviceB); + } + + /// An unresolvable ref keeps the current first-device fallback. + [Fact] + public void ResolveHost_UnknownRef_KeepsCurrentFallback() + { + var drv = NewDriver(); + drv.ResolveHost("totally-unknown").ShouldBe(DeviceA); + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs new file mode 100644 index 00000000..52109f9d --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs @@ -0,0 +1,42 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; + +/// +/// CONV-4 — must resolve an equipment-tag reference +/// (raw TagConfig JSON) to its OWN device host so per-host breaker keys are correct for +/// equipment tags, not the first-device fallback. +/// +[Trait("Category", "Unit")] +public sealed class AbLegacyResolveHostTests +{ + private const string DeviceA = "ab://10.0.0.5/1,0"; + private const string DeviceB = "ab://10.0.0.6/1,0"; + + private static AbLegacyDriver NewDriver() => new( + new AbLegacyDriverOptions + { + Devices = [new AbLegacyDeviceOptions(DeviceA), new AbLegacyDeviceOptions(DeviceB)], + Probe = new AbLegacyProbeOptions { Enabled = false }, + }, + "ablegacy-1", new FakeAbLegacyTagFactory()); + + /// An equipment-tag ref naming device B resolves to device B's host, not the first device. + [Fact] + public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost() + { + var drv = NewDriver(); + var json = $$"""{"deviceHostAddress":"{{DeviceB}}","address":"N7:0","dataType":"Int"}"""; + drv.ResolveHost(json).ShouldBe(DeviceB); + } + + /// An unresolvable ref keeps the current first-device fallback. + [Fact] + public void ResolveHost_UnknownRef_KeepsCurrentFallback() + { + var drv = NewDriver(); + drv.ResolveHost("totally-unknown").ShouldBe(DeviceA); + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasResolveHostTests.cs new file mode 100644 index 00000000..c4827c9c --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasResolveHostTests.cs @@ -0,0 +1,52 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; + +namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; + +/// +/// CONV-4 — must resolve an equipment-tag reference (raw +/// TagConfig JSON) to its OWN device host so per-host breaker keys are correct for equipment +/// tags. An address-less equipment ref (FOCAS coalesces a missing deviceHostAddress to +/// "") must fall back rather than key an empty host. +/// +[Trait("Category", "Unit")] +public sealed class FocasResolveHostTests +{ + private const string DeviceA = "focas://10.0.0.5:8193"; + private const string DeviceB = "focas://10.0.0.6:8193"; + + private static FocasDriver NewDriver() => new( + new FocasDriverOptions + { + Devices = [new FocasDeviceOptions(DeviceA), new FocasDeviceOptions(DeviceB)], + Probe = new FocasProbeOptions { Enabled = false }, + }, + "focas-1", new FakeFocasClientFactory()); + + /// An equipment-tag ref naming device B resolves to device B's host, not the first device. + [Fact] + public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost() + { + var drv = NewDriver(); + var json = $$"""{"deviceHostAddress":"{{DeviceB}}","address":"R100","dataType":"Byte"}"""; + drv.ResolveHost(json).ShouldBe(DeviceB); + } + + /// An equipment ref with no deviceHostAddress falls back to the first device, not an empty host. + [Fact] + public void ResolveHost_EquipmentTagRef_WithoutDeviceHost_FallsBack() + { + var drv = NewDriver(); + var json = """{"address":"R100","dataType":"Byte"}"""; + drv.ResolveHost(json).ShouldBe(DeviceA); + } + + /// An unresolvable ref keeps the current first-device fallback. + [Fact] + public void ResolveHost_UnknownRef_KeepsCurrentFallback() + { + var drv = NewDriver(); + drv.ResolveHost("totally-unknown").ShouldBe(DeviceA); + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATResolveHostTests.cs new file mode 100644 index 00000000..c7a52c07 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATResolveHostTests.cs @@ -0,0 +1,43 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; + +namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; + +/// +/// CONV-4 — must resolve an equipment-tag reference +/// (raw TagConfig JSON) to its OWN device host so per-host breaker keys are correct for +/// equipment tags, not the first-device fallback. +/// +[Trait("Category", "Unit")] +public sealed class TwinCATResolveHostTests +{ + private const string DeviceA = "ads://5.23.91.23.1.1:851"; + private const string DeviceB = "ads://5.23.91.23.1.2:851"; + + private static TwinCATDriver NewDriver() => new( + new TwinCATDriverOptions + { + Devices = [new TwinCATDeviceOptions(DeviceA), new TwinCATDeviceOptions(DeviceB)], + Probe = new TwinCATProbeOptions { Enabled = false }, + EnableControllerBrowse = false, + }, + "twincat-1", new FakeTwinCATClientFactory()); + + /// An equipment-tag ref naming device B resolves to device B's host, not the first device. + [Fact] + public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost() + { + var drv = NewDriver(); + var json = $$"""{"deviceHostAddress":"{{DeviceB}}","symbolPath":"MAIN.Speed","dataType":"DInt"}"""; + drv.ResolveHost(json).ShouldBe(DeviceB); + } + + /// An unresolvable ref keeps the current first-device fallback. + [Fact] + public void ResolveHost_UnknownRef_KeepsCurrentFallback() + { + var drv = NewDriver(); + drv.ResolveHost("totally-unknown").ShouldBe(DeviceA); + } +}