Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDhPlusBridgingTests.cs
2026-04-26 08:56:23 -04:00

148 lines
6.1 KiB
C#

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;
/// <summary>
/// PR ablegacy-13 / #256 — coverage for the 1756-DHRIO DH+ bridge form on
/// <see cref="AbLegacyHostAddress"/>: octal-station validation, slot bounds, port shape,
/// and the PLC-5-only family guard at <c>AbLegacyDriver.InitializeAsync</c>.
/// </summary>
[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<InvalidOperationException>(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);
}
}