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); + } +}