bc985f8857
Makes the N-file array-write case usable as the real-controller hardware gate: AB_LEGACY_ARRAY_WRITE_BASE names the first of six consecutive words (default N7:1), each word is read first and written back in a finally, and the neighbours are verified per word with coalescing off. Live-run green on ab_server slc500/micrologix/plc5 x libplctag/native (39/39), and with a moved base (N7:3). Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
96 lines
5.5 KiB
C#
96 lines
5.5 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.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Review 07 U10: an N-file array write (4 elements) round-trips every element on both transports and leaves the
|
|
/// words either side untouched. Gated by <see cref="IntegrationWriteGate"/>. Doubles as the real-controller
|
|
/// hardware gate: <c>AB_LEGACY_ARRAY_WRITE_BASE</c> (default <c>N7:1</c>) names the first of the SIX consecutive
|
|
/// words it uses (base = "before" sentinel, base+1..base+4 = the array, base+5 = "after" sentinel); every one is
|
|
/// read first and written back in a <c>finally</c>, so a run restores them byte-exact.
|
|
/// </summary>
|
|
[Collection(AbLegacyServerCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Simulator", "ab_server-PCCC")]
|
|
public sealed class AbLegacyArrayWriteIntegrationTests(AbLegacyServerFixture sim)
|
|
{
|
|
/// <summary>The env var naming the first word of the six the test may overwrite (and restores).</summary>
|
|
public const string BaseVar = "AB_LEGACY_ARRAY_WRITE_BASE";
|
|
|
|
/// <summary>Seeds the neighbours, writes an <c>int[]</c> to a 4-element Int array, reads it all back, restores.</summary>
|
|
/// <param name="profile">The simulator profile.</param>
|
|
/// <param name="transport">The transport stack the driver runs on.</param>
|
|
[AbLegacyTheory]
|
|
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
|
|
public async Task N_file_array_write_round_trips_every_element(AbLegacyServerProfile profile, AbLegacyTransport transport)
|
|
{
|
|
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
|
|
IntegrationWriteGate.SkipUnlessWritesAllowed();
|
|
|
|
var baseText = Environment.GetEnvironmentVariable(BaseVar) is { Length: > 0 } b ? b : "N7:1";
|
|
var baseAddr = AbLegacyAddress.TryParse(baseText, profile.Family);
|
|
if (baseAddr is not { FileLetter: "N", FileNumber: int file, BitIndex: null, SubElement: null } || baseAddr.IsIndirect)
|
|
throw new InvalidOperationException($"{BaseVar}='{baseText}' must be a plain N-file word such as N7:200.");
|
|
string At(int offset) => $"N{file}:{baseAddr.WordNumber + offset}";
|
|
|
|
var deviceUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}";
|
|
var words = Enumerable.Range(0, 6)
|
|
.Select(i => new AbLegacyTagDefinition($"W{i}", deviceUri, At(i), AbLegacyDataType.Int))
|
|
.ToArray();
|
|
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions(deviceUri, profile.Family)],
|
|
RawTags = AbLegacyRawTags.Entries(
|
|
[.. words, new AbLegacyTagDefinition("Arr", deviceUri, At(1), AbLegacyDataType.Int, ArrayLength: 4)]),
|
|
Timeout = TimeSpan.FromSeconds(5),
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
Transport = transport,
|
|
CoalesceReads = false, // read each word on its own, independent of the array path under test
|
|
}, driverInstanceId: $"ablegacy-arraywrite-{profile.Family}-{transport}");
|
|
var ct = TestContext.Current.CancellationToken;
|
|
await drv.InitializeAsync("{}", ct);
|
|
string[] wordRefs = [.. words.Select(w => w.Name)];
|
|
|
|
if (profile.Family == AbLegacyPlcFamily.Plc5 && transport == AbLegacyTransport.Native)
|
|
{
|
|
// Native PLC-5 writes are refused before the wire (no capture of the PLC-5 write frames exists).
|
|
(await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 2, 3, 4 })], ct)).Single()
|
|
.StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
|
return;
|
|
}
|
|
|
|
var original = await drv.ReadAsync(wordRefs, ct);
|
|
original.ShouldAllBe(s => s.StatusCode == AbLegacyStatusMapper.Good, "the six words must exist before anything is written");
|
|
try
|
|
{
|
|
(await drv.WriteAsync([new WriteRequest("W0", (short)111), new WriteRequest("W5", (short)666)], ct))
|
|
.ShouldAllBe(r => r.StatusCode == AbLegacyStatusMapper.Good);
|
|
|
|
// ScadaBridge shape: a CLR int[] to an Int16 array node.
|
|
(await drv.WriteAsync([new WriteRequest("Arr", new[] { 10, -20, 30, 32767 })], ct)).Single()
|
|
.StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
|
|
var arr = (await drv.ReadAsync(["Arr"], ct)).Single();
|
|
arr.StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
arr.Value.ShouldBe(new short[] { 10, -20, 30, 32767 });
|
|
var back = await drv.ReadAsync(wordRefs, ct);
|
|
back.Select(s => Convert.ToInt32(s.Value)).ShouldBe(new[] { 111, 10, -20, 30, 32767, 666 }); // per word, neighbours untouched
|
|
|
|
// A length mismatch is refused before the wire and changes nothing.
|
|
(await drv.WriteAsync([new WriteRequest("Arr", new[] { 1, 2, 3 })], ct)).Single()
|
|
.StatusCode.ShouldBe(AbLegacyStatusMapper.BadOutOfRange);
|
|
(await drv.ReadAsync(["Arr"], ct)).Single().Value.ShouldBe(new short[] { 10, -20, 30, 32767 });
|
|
}
|
|
finally
|
|
{
|
|
// Restore all six words exactly as found (CancellationToken.None: restore even if the run was cancelled).
|
|
var restore = words.Select((w, i) => new WriteRequest(w.Name, Convert.ToInt16(original[i].Value))).ToArray();
|
|
(await drv.WriteAsync(restore, CancellationToken.None))
|
|
.ShouldAllBe(r => r.StatusCode == AbLegacyStatusMapper.Good, "restoring the original words must succeed");
|
|
}
|
|
}
|
|
}
|