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.2 — coverage for the per-device AddressingMode toggle. /// Asserts (a) resolves to /// at the device level, (b) explicit /// threads through every /// the driver builds, (c) Logical against an unsupported /// family (Micro800) emits a warning + falls back to Symbolic, (d) the Driver-config DTO /// round-trips the mode, and (e) family compatibility is captured by /// . /// [Trait("Category", "Unit")] public sealed class AbCipAddressingModeTests { // ---- Auto resolves to Symbolic ---- [Fact] public async Task Default_AddressingMode_resolves_to_Symbolic_on_DeviceState() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic); } [Fact] public async Task Auto_AddressingMode_resolves_to_Symbolic_on_DeviceState() { var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions( HostAddress: "ab://10.0.0.5/1,0", PlcFamily: AbCipPlcFamily.ControlLogix, AddressingMode: AddressingMode.Auto), ], Probe = new AbCipProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic); } // ---- Logical threads through to AbCipTagCreateParams ---- [Fact] public async Task Logical_AddressingMode_threads_through_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, AddressingMode: AddressingMode.Logical), ], Probe = new AbCipProbeOptions { Enabled = false }, Tags = [ new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt), ], }, "drv-1", tagFactory: factory, enumeratorFactory: new EmptyEnumeratorFactoryStub()); await drv.InitializeAsync("{}", CancellationToken.None); await drv.ReadAsync(["Speed"], CancellationToken.None); factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Logical); } [Fact] public async Task Symbolic_AddressingMode_explicitly_set_threads_through() { var factory = new FakeAbCipTagFactory(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions( HostAddress: "ab://10.0.0.5/1,0", PlcFamily: AbCipPlcFamily.ControlLogix, AddressingMode: AddressingMode.Symbolic), ], 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.AddressingMode.ShouldBe(AddressingMode.Symbolic); } // ---- Logical against unsupported family falls back with warning ---- [Fact] public async Task Logical_on_Micro800_falls_back_to_Symbolic_with_warning() { var warnings = new List(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions( HostAddress: "ab://10.0.0.6/", PlcFamily: AbCipPlcFamily.Micro800, AddressingMode: AddressingMode.Logical), ], Probe = new AbCipProbeOptions { Enabled = false }, OnWarning = warnings.Add, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); drv.GetDeviceState("ab://10.0.0.6/")!.AddressingMode.ShouldBe(AddressingMode.Symbolic); warnings.ShouldHaveSingleItem(); warnings[0].ShouldContain("Micro800"); warnings[0].ShouldContain("Logical"); } [Fact] public async Task Logical_on_Micro800_carries_Symbolic_into_create_params() { var factory = new FakeAbCipTagFactory(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions( HostAddress: "ab://10.0.0.6/", PlcFamily: AbCipPlcFamily.Micro800, AddressingMode: AddressingMode.Logical), ], Probe = new AbCipProbeOptions { Enabled = false }, Tags = [ new AbCipTagDefinition("Speed", "ab://10.0.0.6/", "Speed", AbCipDataType.DInt), ], OnWarning = _ => { }, }, "drv-1", tagFactory: factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.ReadAsync(["Speed"], CancellationToken.None); factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Symbolic); } [Fact] public async Task Logical_on_ControlLogix_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.ControlLogix, AddressingMode: AddressingMode.Logical), ], Probe = new AbCipProbeOptions { Enabled = false }, OnWarning = warnings.Add, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); warnings.ShouldBeEmpty(); drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical); } // ---- Family-profile compatibility flags ---- [Fact] public void Family_profiles_advertise_logical_support_correctly() { AbCipPlcFamilyProfile.ControlLogix.SupportsLogicalAddressing.ShouldBeTrue(); AbCipPlcFamilyProfile.CompactLogix.SupportsLogicalAddressing.ShouldBeTrue(); AbCipPlcFamilyProfile.GuardLogix.SupportsLogicalAddressing.ShouldBeTrue(); AbCipPlcFamilyProfile.Micro800.SupportsLogicalAddressing.ShouldBeFalse(); } // ---- DTO round-trip ---- [Fact] public async Task DTO_round_trips_AddressingMode_Logical_through_config_json() { var json = """ { "Devices": [ { "HostAddress": "ab://10.0.0.5/1,0", "PlcFamily": "ControlLogix", "AddressingMode": "Logical" } ], "Probe": { "Enabled": false } } """; var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json); await drv.InitializeAsync(json, CancellationToken.None); drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical); } [Fact] public async Task DTO_round_trips_AddressingMode_Symbolic_through_config_json() { var json = """ { "Devices": [ { "HostAddress": "ab://10.0.0.5/1,0", "PlcFamily": "ControlLogix", "AddressingMode": "Symbolic" } ], "Probe": { "Enabled": false } } """; var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json); await drv.InitializeAsync(json, CancellationToken.None); drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic); } [Fact] public async Task DTO_omitted_AddressingMode_falls_back_to_Auto_then_Symbolic() { // No AddressingMode in JSON → DTO field is null → factory parses fallback Auto → // device-level resolution lands on Symbolic. var json = """ { "Devices": [ { "HostAddress": "ab://10.0.0.5/1,0", "PlcFamily": "ControlLogix" } ], "Probe": { "Enabled": false } } """; var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json); await drv.InitializeAsync(json, CancellationToken.None); drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic); } // ---- Logical-mode triggers a one-time symbol walk ---- [Fact] public async Task Logical_mode_first_read_triggers_symbol_walk_once() { var enumStub = new RecordingEnumeratorFactory( new AbCipDiscoveredTag("Speed", null, AbCipDataType.DInt, false), new AbCipDiscoveredTag("Counter", null, AbCipDataType.DInt, false)); var factory = new FakeAbCipTagFactory(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions( HostAddress: "ab://10.0.0.5/1,0", PlcFamily: AbCipPlcFamily.ControlLogix, AddressingMode: AddressingMode.Logical), ], Probe = new AbCipProbeOptions { Enabled = false }, Tags = [ new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt), new AbCipTagDefinition("Counter", "ab://10.0.0.5/1,0", "Counter", AbCipDataType.DInt), ], }, "drv-1", tagFactory: factory, enumeratorFactory: enumStub); await drv.InitializeAsync("{}", CancellationToken.None); // First read fires the walk await drv.ReadAsync(["Speed"], CancellationToken.None); // Second read must NOT walk again await drv.ReadAsync(["Counter"], CancellationToken.None); enumStub.CreateCount.ShouldBe(1); var device = drv.GetDeviceState("ab://10.0.0.5/1,0")!; device.LogicalWalkComplete.ShouldBeTrue(); device.LogicalInstanceMap.ShouldContainKey("Speed"); device.LogicalInstanceMap.ShouldContainKey("Counter"); } [Fact] public async Task Symbolic_mode_does_not_trigger_symbol_walk() { var enumStub = new RecordingEnumeratorFactory(); var factory = new FakeAbCipTagFactory(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [ new AbCipDeviceOptions( HostAddress: "ab://10.0.0.5/1,0", PlcFamily: AbCipPlcFamily.ControlLogix, AddressingMode: AddressingMode.Symbolic), ], Probe = new AbCipProbeOptions { Enabled = false }, Tags = [ new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt), ], }, "drv-1", tagFactory: factory, enumeratorFactory: enumStub); await drv.InitializeAsync("{}", CancellationToken.None); await drv.ReadAsync(["Speed"], CancellationToken.None); enumStub.CreateCount.ShouldBe(0); drv.GetDeviceState("ab://10.0.0.5/1,0")!.LogicalWalkComplete.ShouldBeFalse(); } // ---- Stubs ---- private sealed class EmptyEnumeratorFactoryStub : IAbCipTagEnumeratorFactory { public IAbCipTagEnumerator Create() => new EmptyStub(); private sealed class EmptyStub : IAbCipTagEnumerator { public async IAsyncEnumerable EnumerateAsync( AbCipTagCreateParams deviceParams, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await Task.CompletedTask; yield break; } public void Dispose() { } } } private sealed class RecordingEnumeratorFactory : IAbCipTagEnumeratorFactory { private readonly AbCipDiscoveredTag[] _seed; public int CreateCount; public RecordingEnumeratorFactory(params AbCipDiscoveredTag[] seed) => _seed = seed; public IAbCipTagEnumerator Create() { CreateCount++; return new SeededStub(_seed); } private sealed class SeededStub(AbCipDiscoveredTag[] seed) : IAbCipTagEnumerator { public async IAsyncEnumerable EnumerateAsync( AbCipTagCreateParams deviceParams, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { foreach (var tag in seed) yield return tag; await Task.CompletedTask; } public void Dispose() { } } } }