Merge b640-646 (AbLegacy indirect addressing #646)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+55
@@ -0,0 +1,55 @@
|
||||
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.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// #646 against <c>ab_server</c>: <c>N7:[N7:0]</c> reads the index word, then the word it names, on both
|
||||
/// transports. Writes N7:0 and N7:5 (simulator-only, gated by <see cref="IntegrationWriteGate"/>).
|
||||
/// </summary>
|
||||
[Collection(AbLegacyServerCollection.Name)]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Simulator", "ab_server-PCCC")]
|
||||
public sealed class AbLegacyIndirectAddressingTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
/// <summary>Seeds N7:0=5 and N7:5=42, then reads and writes through <c>N7:[N7:0]</c>.</summary>
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbLegacyTheory]
|
||||
[MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
|
||||
public async Task Slc500_indirect_word_read_and_write_resolve_through_the_index(AbLegacyTransport transport)
|
||||
{
|
||||
if (sim.SkipReasonFor(KnownProfiles.Slc500) is { } skip) Assert.Skip(skip);
|
||||
IntegrationWriteGate.SkipUnlessWritesAllowed();
|
||||
|
||||
var deviceUri = $"ab://{sim.Host}:{sim.PortFor(KnownProfiles.Slc500)}/{sim.CipPath}";
|
||||
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(deviceUri, AbLegacyPlcFamily.Slc500)],
|
||||
RawTags = AbLegacyRawTags.Entries(
|
||||
new AbLegacyTagDefinition("Index", deviceUri, "N7:0", AbLegacyDataType.Int, Writable: true),
|
||||
new AbLegacyTagDefinition("Target", deviceUri, "N7:5", AbLegacyDataType.Int, Writable: true),
|
||||
new AbLegacyTagDefinition("Indirect", deviceUri, "N7:[N7:0]", AbLegacyDataType.Int, Writable: true)),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
Transport = transport,
|
||||
}, driverInstanceId: $"ablegacy-indirect-{transport}");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await drv.InitializeAsync("{}", ct);
|
||||
|
||||
var seeded = await drv.WriteAsync(
|
||||
[new WriteRequest("Index", (short)5), new WriteRequest("Target", (short)42)], ct);
|
||||
seeded.ShouldAllBe(r => r.StatusCode == AbLegacyStatusMapper.Good);
|
||||
|
||||
var read = (await drv.ReadAsync(["Indirect"], ct)).Single();
|
||||
read.StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
Convert.ToInt32(read.Value).ShouldBe(42);
|
||||
|
||||
// Write through the indirection lands on N7:5.
|
||||
(await drv.WriteAsync([new WriteRequest("Indirect", (short)77)], ct)).Single()
|
||||
.StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
var direct = (await drv.ReadAsync(["Target"], ct)).Single();
|
||||
Convert.ToInt32(direct.Value).ShouldBe(77);
|
||||
}
|
||||
}
|
||||
-22
@@ -181,28 +181,6 @@ public sealed class AbLegacyFamilyAddressingTests
|
||||
factory.Tags["I:15/15"].ReadCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>An indirect tag is answered BadNotSupported on read and write, and no runtime is ever created for it.</summary>
|
||||
[Fact]
|
||||
public async Task Indirect_tag_is_BadNotSupported_and_creates_no_runtime()
|
||||
{
|
||||
var (drv, factory) = NewDriver(AbLegacyPlcFamily.Slc500,
|
||||
new AbLegacyTagDefinition("Ind", "ab://10.0.0.5/1,0", "N7:[N7:0]", AbLegacyDataType.Int, Writable: true),
|
||||
new AbLegacyTagDefinition("Ok", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int, Writable: true));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = p => new FakeAbLegacyTag(p) { Value = 5 };
|
||||
|
||||
var read = await drv.ReadAsync(["Ind", "Ok"], CancellationToken.None);
|
||||
var write = await drv.WriteAsync(
|
||||
[new WriteRequest("Ind", 1), new WriteRequest("Ok", 1)], CancellationToken.None);
|
||||
|
||||
read[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
||||
write[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
||||
// positive control: the neighbouring direct tag in the same batch still works, so the refusal is per-tag
|
||||
read[1].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
write[1].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
factory.Tags.Keys.ShouldNotContain(k => k.Contains('['));
|
||||
}
|
||||
|
||||
/// <summary>A MicroLogix function-file tag reaches the transport under its libplctag name.</summary>
|
||||
[Fact]
|
||||
public async Task MicroLogix_function_file_tag_reaches_the_transport()
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,10 @@ public sealed class AbLegacyFamilyAddressGateTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate_warns_about_indirect_addresses_which_never_read()
|
||||
public void Gate_does_not_warn_about_indirect_addresses_which_the_driver_resolves()
|
||||
{
|
||||
EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg("N7:[N7:0]"), "{\"PlcFamily\":\"Slc500\"}")
|
||||
.ShouldContain(x => x.Contains("indirect"));
|
||||
.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
Reference in New Issue
Block a user