Auto: abcip-3.1 — configurable CIP connection size per device
Closes #235
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.1 — coverage for the per-device CIP <c>ConnectionSize</c> override.
|
||||
/// Asserts (a) the value flows from <see cref="AbCipDeviceOptions"/> into every
|
||||
/// <see cref="AbCipTagCreateParams"/> the driver builds, (b) the family default kicks in
|
||||
/// when the override is unset, (c) values outside the Kepware-supported range are rejected
|
||||
/// at <c>InitializeAsync</c>, and (d) the legacy-firmware warning fires when a CompactLogix
|
||||
/// narrow-cap device is configured above 511 bytes.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipConnectionSizeTests
|
||||
{
|
||||
// ---- options threading ----
|
||||
|
||||
[Fact]
|
||||
public async Task Custom_ConnectionSize_flows_from_device_options_into_create_params()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
ConnectionSize: 1500),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition(
|
||||
Name: "Speed",
|
||||
DeviceHostAddress: "ab://10.0.0.5/1,0",
|
||||
TagPath: "Speed",
|
||||
DataType: AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(1500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unset_ConnectionSize_falls_back_to_ControlLogix_family_default()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize
|
||||
.ShouldBe(AbCipPlcFamilyProfile.ControlLogix.DefaultConnectionSize);
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(4002);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unset_ConnectionSize_falls_back_to_CompactLogix_family_default()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.CompactLogix)],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(504);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unset_ConnectionSize_falls_back_to_Micro800_family_default()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.6/", AbCipPlcFamily.Micro800)],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.6/", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.ConnectionSize.ShouldBe(488);
|
||||
}
|
||||
|
||||
// ---- range validation ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(499)]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(4003)]
|
||||
[InlineData(10000)]
|
||||
public async Task Out_of_range_ConnectionSize_throws_at_InitializeAsync(int badSize)
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
ConnectionSize: badSize),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
() => drv.InitializeAsync("{}", CancellationToken.None));
|
||||
ex.Message.ShouldContain("ConnectionSize");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(500)]
|
||||
[InlineData(504)]
|
||||
[InlineData(2000)]
|
||||
[InlineData(4002)]
|
||||
public async Task In_range_ConnectionSize_initialises_cleanly(int goodSize)
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
ConnectionSize: goodSize),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(goodSize);
|
||||
}
|
||||
|
||||
// ---- legacy-firmware warning ----
|
||||
|
||||
[Fact]
|
||||
public async Task Oversized_ConnectionSize_on_CompactLogix_emits_legacy_warning()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.CompactLogix,
|
||||
ConnectionSize: 1500),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
warnings.ShouldHaveSingleItem();
|
||||
warnings[0].ShouldContain("CompactLogix");
|
||||
warnings[0].ShouldContain("1500");
|
||||
warnings[0].ShouldContain("Forward Open");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Within_legacy_cap_on_CompactLogix_does_not_warn()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.CompactLogix,
|
||||
ConnectionSize: 504),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
warnings.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Oversized_ConnectionSize_on_ControlLogix_does_not_warn()
|
||||
{
|
||||
// ControlLogix profile default is 4002 (Large Forward Open) — the warning is only
|
||||
// meaningful when the family default is in the legacy-cap bucket. FW20+ ControlLogix
|
||||
// happily accepts 1500-byte connections, so no warning fires.
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
ConnectionSize: 1500),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
warnings.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Oversized_ConnectionSize_on_Micro800_emits_legacy_warning()
|
||||
{
|
||||
// Micro800 default is 488 (well under the legacy cap), so any over-511 override
|
||||
// triggers the same family-mismatch warning.
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.6/",
|
||||
PlcFamily: AbCipPlcFamily.Micro800,
|
||||
ConnectionSize: 1000),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
warnings.ShouldHaveSingleItem();
|
||||
warnings[0].ShouldContain("Micro800");
|
||||
}
|
||||
|
||||
// ---- DeviceState resolved ConnectionSize ----
|
||||
|
||||
[Fact]
|
||||
public async Task DeviceState_ConnectionSize_reflects_override_when_set()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix, ConnectionSize: 2000),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(2000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeviceState_ConnectionSize_reflects_family_default_when_unset()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.CompactLogix)],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(504);
|
||||
}
|
||||
|
||||
// ---- AbCipConnectionSize constants ----
|
||||
|
||||
[Fact]
|
||||
public void Constants_match_documented_Kepware_range()
|
||||
{
|
||||
AbCipConnectionSize.Min.ShouldBe(500);
|
||||
AbCipConnectionSize.Max.ShouldBe(4002);
|
||||
AbCipConnectionSize.LegacyFirmwareCap.ShouldBe(511);
|
||||
}
|
||||
|
||||
// ---- DriverConfig DTO path (DriverFactoryRegistry-bound deployments) ----
|
||||
|
||||
[Fact]
|
||||
public async Task Driver_factory_threads_ConnectionSize_through_config_json()
|
||||
{
|
||||
// The bootstrapper-driven path deserialises driver config from JSON in the central
|
||||
// DB (sp_PublishGeneration → DriverInstance.DriverConfig). The DTO must surface
|
||||
// ConnectionSize so production deployments don't lose the override at the wire.
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix",
|
||||
"ConnectionSize": 1500
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
// CreateInstance returns a fully-built driver; we kick InitializeAsync to surface the
|
||||
// resolved DeviceState.ConnectionSize.
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.ConnectionSize.ShouldBe(1500);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user