b0b0ce9c77
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
132 lines
5.3 KiB
C#
132 lines
5.3 KiB
C#
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;
|
|
|
|
/// <summary>#646: indirect addresses (N7:[N7:0], N[N7:0]:5) resolve at read/write time.</summary>
|
|
[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);
|
|
}
|
|
}
|