using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// #646: indirect addresses (N7:[N7:0], N[N7:0]:5) resolve at read/write time. [Trait("Category", "Unit")] public sealed class AbLegacyIndirectAddressingTests { private const string Host = "ab://10.0.0.5/1,0"; private static async Task<(AbLegacyDriver drv, FakeAbLegacyTagFactory factory)> NewDriverAsync( params AbLegacyTagDefinition[] tags) { var factory = new FakeAbLegacyTagFactory(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(Host, AbLegacyPlcFamily.Slc500)], RawTags = AbLegacyRawTags.Entries(tags), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); return (drv, factory); } private static void Seed(FakeAbLegacyTagFactory f, params (string name, object value)[] values) { var map = values.ToDictionary(v => v.name, v => v.value, StringComparer.OrdinalIgnoreCase); f.Customise = p => new FakeAbLegacyTag(p) { Value = map.TryGetValue(p.TagName, out var v) ? v : 0 }; } [Fact] public async Task Word_indirect_reads_index_then_composed_address() { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N7:[N7:0]", AbLegacyDataType.Int)); Seed(f, ("N7:0", (short)5), ("N7:5", (short)42)); var r = (await drv.ReadAsync(["Ind"], CancellationToken.None)).Single(); r.StatusCode.ShouldBe(AbLegacyStatusMapper.Good); r.Value.ShouldBe((short)42); f.Tags["N7:0"].ReadCount.ShouldBe(1); f.Tags["N7:5"].ReadCount.ShouldBe(1); f.Tags.Keys.ShouldNotContain(k => k.Contains('[')); } [Fact] public async Task File_indirect_resolves_the_file_number() { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N[N7:0]:5", AbLegacyDataType.Int)); Seed(f, ("N7:0", (short)9), ("N9:5", (short)77)); var r = (await drv.ReadAsync(["Ind"], CancellationToken.None)).Single(); r.StatusCode.ShouldBe(AbLegacyStatusMapper.Good); r.Value.ShouldBe((short)77); f.Tags.ContainsKey("N9:5").ShouldBeTrue(); } [Fact] public async Task Index_is_re_read_every_poll_never_cached() { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N7:[N7:0]", AbLegacyDataType.Int)); Seed(f, ("N7:0", (short)5), ("N7:5", (short)42), ("N7:6", (short)43)); (await drv.ReadAsync(["Ind"], CancellationToken.None)).Single().Value.ShouldBe((short)42); f.Tags["N7:0"].Value = (short)6; (await drv.ReadAsync(["Ind"], CancellationToken.None)).Single().Value.ShouldBe((short)43); f.Tags["N7:0"].ReadCount.ShouldBe(2); // the stale resolution's handle is released when the index moves f.Tags["N7:5"].Disposed.ShouldBeTrue(); } [Theory] [InlineData(-1)] [InlineData(256)] [InlineData(30000)] public async Task Out_of_range_index_is_BadOutOfRange_without_health_impact(int index) { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N7:[N7:0]", AbLegacyDataType.Int)); Seed(f, ("N7:0", (short)index)); var r = (await drv.ReadAsync(["Ind"], CancellationToken.None)).Single(); r.StatusCode.ShouldBe(AbLegacyStatusMapper.BadOutOfRange); drv.GetHealth().State.ShouldNotBe(DriverState.Degraded); f.Tags.Keys.ShouldNotContain(k => k.StartsWith("N7:") && k != "N7:0"); } [Fact] public async Task Inner_read_failure_propagates_its_status() { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N7:[N7:0]", AbLegacyDataType.Int)); f.Customise = p => new FakeAbLegacyTag(p) { Value = (short)5, Status = p.TagName == "N7:0" ? -14 : 0 }; var r = (await drv.ReadAsync(["Ind"], CancellationToken.None)).Single(); r.StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good); r.StatusCode.ShouldNotBe(AbLegacyStatusMapper.BadOutOfRange); f.Tags.ContainsKey("N7:5").ShouldBeFalse(); } [Fact] public async Task Write_resolves_index_then_writes_the_composed_address() { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N7:[N7:0]", AbLegacyDataType.Int, Writable: true)); Seed(f, ("N7:0", (short)5)); var w = (await drv.WriteAsync([new WriteRequest("Ind", 99)], CancellationToken.None)).Single(); w.StatusCode.ShouldBe(AbLegacyStatusMapper.Good); f.Tags["N7:5"].WriteCount.ShouldBe(1); f.Tags["N7:5"].Value.ShouldBe((short)99); f.Tags["N7:0"].WriteCount.ShouldBe(0); } [Fact] public async Task Write_with_bad_index_is_BadOutOfRange_and_writes_nothing() { var (drv, f) = await NewDriverAsync(new AbLegacyTagDefinition("Ind", Host, "N7:[N7:0]", AbLegacyDataType.Int, Writable: true)); Seed(f, ("N7:0", (short)-3)); var w = (await drv.WriteAsync([new WriteRequest("Ind", 1)], CancellationToken.None)).Single(); w.StatusCode.ShouldBe(AbLegacyStatusMapper.BadOutOfRange); f.Tags.Values.ShouldAllBe(t => t.WriteCount == 0); } }