using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; [Trait("Category", "Unit")] public sealed class AbLegacyReadWriteTests { private static (AbLegacyDriver drv, FakeAbLegacyTagFactory factory) NewDriver(params AbLegacyTagDefinition[] tags) { var factory = new FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], Tags = tags, }, "drv-1", factory); return (drv, factory); } // ---- Read ---- [Fact] public async Task Unknown_reference_maps_to_BadNodeIdUnknown() { var (drv, _) = NewDriver(); await drv.InitializeAsync("{}", CancellationToken.None); var snapshots = await drv.ReadAsync(["missing"], CancellationToken.None); snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown); } [Fact] public async Task Successful_N_file_read_returns_Good_value() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("Counter", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { Value = 42 }; var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None); snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); snapshots.Single().Value.ShouldBe(42); factory.Tags["N7:0"].InitializeCount.ShouldBe(1); factory.Tags["N7:0"].ReadCount.ShouldBe(1); } [Fact] public async Task Repeat_read_reuses_runtime() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 }; await drv.ReadAsync(["X"], CancellationToken.None); await drv.ReadAsync(["X"], CancellationToken.None); factory.Tags["N7:0"].InitializeCount.ShouldBe(1); factory.Tags["N7:0"].ReadCount.ShouldBe(2); } [Fact] public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { Status = -14 }; var snapshots = await drv.ReadAsync(["X"], CancellationToken.None); snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown); } [Fact] public async Task Read_exception_surfaces_BadCommunicationError() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnRead = true }; var snapshots = await drv.ReadAsync(["X"], CancellationToken.None); snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError); drv.GetHealth().State.ShouldBe(DriverState.Degraded); } [Fact] public async Task Batched_reads_preserve_order() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int), new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "F8:0", AbLegacyDataType.Float), new AbLegacyTagDefinition("C", "ab://10.0.0.5/1,0", "ST9:0", AbLegacyDataType.String)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => p.TagName switch { "N7:0" => new FakeAbLegacyTag(p) { Value = 1 }, "F8:0" => new FakeAbLegacyTag(p) { Value = 3.14f }, _ => new FakeAbLegacyTag(p) { Value = "hello" }, }; var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None); snapshots.Count.ShouldBe(3); snapshots[0].Value.ShouldBe(1); snapshots[1].Value.ShouldBe(3.14f); snapshots[2].Value.ShouldBe("hello"); } [Fact] public async Task Read_TagCreateParams_composed_from_device_and_profile() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:5", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); await drv.ReadAsync(["X"], CancellationToken.None); var p = factory.Tags["N7:5"].CreationParams; p.Gateway.ShouldBe("10.0.0.5"); p.Port.ShouldBe(44818); p.CipPath.ShouldBe("1,0"); p.LibplctagPlcAttribute.ShouldBe("slc500"); p.TagName.ShouldBe("N7:5"); } // ---- Write ---- [Fact] public async Task Non_writable_tag_rejects_with_BadNotWritable() { var (drv, _) = NewDriver( new AbLegacyTagDefinition("RO", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, Writable: false)); await drv.InitializeAsync("{}", CancellationToken.None); var results = await drv.WriteAsync( [new WriteRequest("RO", 1)], CancellationToken.None); results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable); } [Fact] public async Task Successful_N_file_write_encodes_and_flushes() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); var results = await drv.WriteAsync( [new WriteRequest("X", 123)], CancellationToken.None); results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); factory.Tags["N7:0"].Value.ShouldBe(123); factory.Tags["N7:0"].WriteCount.ShouldBe(1); } [Fact] public async Task Bit_within_word_write_now_succeeds_via_RMW() { // Task #181 pass 2 lifted this gap — N-file bit writes now go through // WriteBitInWordAsync + a parallel parent-word runtime, so the status is Good rather // than BadNotSupported. Full RMW semantics covered by AbLegacyBitRmwTests. var factory = new FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], Tags = [new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)], }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); var results = await drv.WriteAsync( [new WriteRequest("Bit3", true)], CancellationToken.None); results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); } [Fact] public async Task Write_exception_surfaces_BadCommunicationError() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnWrite = true }; var results = await drv.WriteAsync( [new WriteRequest("X", 1)], CancellationToken.None); results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError); } [Fact] public async Task Batch_write_preserves_order_across_outcomes() { var factory = new FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], Tags = [ new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int), new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int, Writable: false), ], }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); var results = await drv.WriteAsync( [ new WriteRequest("A", 1), new WriteRequest("B", 2), new WriteRequest("Unknown", 3), ], CancellationToken.None); results.Count.ShouldBe(3); results[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good); results[1].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable); results[2].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown); } [Fact] public async Task Cancellation_propagates() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnRead = true, Exception = new OperationCanceledException(), }; await Should.ThrowAsync( () => drv.ReadAsync(["X"], CancellationToken.None)); } [Fact] public async Task ShutdownAsync_disposes_runtimes() { var (drv, factory) = NewDriver( new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 }; await drv.ReadAsync(["A"], CancellationToken.None); await drv.ShutdownAsync(CancellationToken.None); factory.Tags["N7:0"].Disposed.ShouldBeTrue(); } private sealed class RmwThrowingFake(AbLegacyTagCreateParams p) : FakeAbLegacyTag(p) { public override void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) { if (type == AbLegacyDataType.Bit && bitIndex is not null) throw new NotSupportedException("bit-within-word RMW deferred"); Value = value; } } }