using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// /// PR ablegacy-13 / #256 — coverage for the 1756-DHRIO DH+ bridge form on /// : octal-station validation, slot bounds, port shape, /// and the PLC-5-only family guard at AbLegacyDriver.InitializeAsync. /// [Trait("Category", "Unit")] public sealed class AbLegacyDhPlusBridgingTests { [Theory] // station 07 octal → 7 decimal [InlineData("ab://10.0.0.1/1,3,2,07", 3, 2, 7)] // station 77 octal → 63 decimal (top of the DH+ address range) [InlineData("ab://10.0.0.1/1,3,2,77", 3, 2, 63)] // single-digit station 0..7 [InlineData("ab://10.0.0.1/1,0,2,0", 0, 2, 0)] [InlineData("ab://10.0.0.1/1,16,2,77", 16, 2, 63)] // station 10 octal → 8 decimal [InlineData("ab://10.0.0.1/1,3,2,10", 3, 2, 8)] public void DhPlusBridge_parses_valid_octal_station(string input, int slot, int dhPort, int station) { var parsed = AbLegacyHostAddress.TryParse(input); parsed.ShouldNotBeNull(); parsed.IsDhPlusBridge.ShouldBeTrue(); parsed.BackplaneSlot.ShouldBe(slot); parsed.DhPlusPort.ShouldBe(dhPort); parsed.DhPlusStation.ShouldBe(station); } [Theory] // octal-illegal digits 8 / 9 in the station segment [InlineData("ab://10.0.0.1/1,3,2,80")] [InlineData("ab://10.0.0.1/1,3,2,90")] [InlineData("ab://10.0.0.1/1,3,2,08")] [InlineData("ab://10.0.0.1/1,3,2,79")] // port-1 not the backplane (must be exactly 1) [InlineData("ab://10.0.0.1/0,3,2,77")] [InlineData("ab://10.0.0.1/2,3,2,77")] // port-3 not the DH+ port (must be exactly 2) [InlineData("ab://10.0.0.1/1,3,3,77")] [InlineData("ab://10.0.0.1/1,3,1,77")] // slot out of range (max 16) [InlineData("ab://10.0.0.1/1,17,2,07")] [InlineData("ab://10.0.0.1/1,99,2,07")] [InlineData("ab://10.0.0.1/1,-1,2,07")] // wrong segment count [InlineData("ab://10.0.0.1/1,3,2")] [InlineData("ab://10.0.0.1/1,3,2,07,extra")] public void DhPlusBridge_rejects_invalid_form(string input) { // The host-address parser still succeeds (the CIP path stays opaque) — only the // DH+ accessors are absent. That keeps AB-Legacy compatible with paths shaped // similarly to a DHRIO bridge but not actually one (e.g. a future DHRIO-clone // module on a non-backplane-1 port). var parsed = AbLegacyHostAddress.TryParse(input); // For the wrong-segment-count and trailing-extra cases, parsing still produces a // generic record with an opaque CipPath and no DH+ surface. For the octal-illegal // and slot/port out-of-range cases the same applies — the CIP path is preserved // verbatim for libplctag, but the structured DH+ accessors are null because the // path didn't pass the strict bridge-form validator. parsed.ShouldNotBeNull(); parsed.IsDhPlusBridge.ShouldBeFalse(); parsed.BackplaneSlot.ShouldBeNull(); parsed.DhPlusPort.ShouldBeNull(); parsed.DhPlusStation.ShouldBeNull(); } [Fact] public void DhPlusBridge_diagnostics_surface_all_three_fields() { var parsed = AbLegacyHostAddress.TryParse("ab://10.0.0.1/1,7,2,42"); parsed.ShouldNotBeNull(); parsed.IsDhPlusBridge.ShouldBeTrue(); parsed.BackplaneSlot.ShouldBe(7); parsed.DhPlusPort.ShouldBe(2); // 42 octal == 4*8 + 2 == 34 decimal parsed.DhPlusStation.ShouldBe(34); // CIP path is preserved verbatim — libplctag receives the original octal-station // representation, not the decimal-translated one. parsed.CipPath.ShouldBe("1,7,2,42"); } [Fact] public void NonBridgePath_leaves_dhplus_fields_null() { // Direct-wired SLC 5/05 — empty path var direct = AbLegacyHostAddress.TryParse("ab://10.0.0.1/"); direct.ShouldNotBeNull(); direct.IsDhPlusBridge.ShouldBeFalse(); direct.BackplaneSlot.ShouldBeNull(); direct.DhPlusPort.ShouldBeNull(); direct.DhPlusStation.ShouldBeNull(); // Standard ControlLogix backplane bridge — only two segments, not four var twoHop = AbLegacyHostAddress.TryParse("ab://10.0.0.1/1,0"); twoHop.ShouldNotBeNull(); twoHop.IsDhPlusBridge.ShouldBeFalse(); twoHop.BackplaneSlot.ShouldBeNull(); } [Theory] [InlineData(AbLegacyPlcFamily.Slc500)] [InlineData(AbLegacyPlcFamily.MicroLogix)] [InlineData(AbLegacyPlcFamily.LogixPccc)] public async Task DhPlusBridge_with_non_plc5_family_throws(AbLegacyPlcFamily nonPlc5) { var options = new AbLegacyDriverOptions { Devices = new[] { new AbLegacyDeviceOptions( HostAddress: "ab://10.0.0.1/1,3,2,07", PlcFamily: nonPlc5), }, }; var driver = new AbLegacyDriver(options, "drv-test", new FakeAbLegacyTagFactory()); var ex = await Should.ThrowAsync(async () => await driver.InitializeAsync("{}", CancellationToken.None)); ex.Message.ShouldContain("DHRIO"); ex.Message.ShouldContain("PLC-5-only"); } [Fact] public async Task DhPlusBridge_with_plc5_family_initialises_cleanly() { var options = new AbLegacyDriverOptions { Devices = new[] { new AbLegacyDeviceOptions( HostAddress: "ab://10.0.0.1/1,3,2,07", PlcFamily: AbLegacyPlcFamily.Plc5), }, // Probe disabled so InitializeAsync doesn't try to spin a probe loop against a // non-existent gateway; the only thing under test is the family validation. Probe = new AbLegacyProbeOptions { Enabled = false }, }; var driver = new AbLegacyDriver(options, "drv-test", new FakeAbLegacyTagFactory()); await driver.InitializeAsync("{}", CancellationToken.None); } }