148 lines
6.1 KiB
C#
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);
|
|
}
|
|
}
|