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); } }