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;
///
/// PR abcip-3.1 — coverage for the per-device CIP ConnectionSize override.
/// Asserts (a) the value flows from into every
/// the driver builds, (b) the family default kicks in
/// when the override is unset, (c) values outside the Kepware-supported range are rejected
/// at InitializeAsync, and (d) the legacy-firmware warning fires when a CompactLogix
/// narrow-cap device is configured above 511 bytes.
///
[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(
() => 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();
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();
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();
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();
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);
}
}