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;
///
/// 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 . Doubles as the real-controller
/// hardware gate: AB_LEGACY_ARRAY_WRITE_BASE (default N7:1) 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 finally, so a run restores them byte-exact.
///
[Collection(AbLegacyServerCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Simulator", "ab_server-PCCC")]
public sealed class AbLegacyArrayWriteIntegrationTests(AbLegacyServerFixture sim)
{
/// The env var naming the first word of the six the test may overwrite (and restores).
public const string BaseVar = "AB_LEGACY_ARRAY_WRITE_BASE";
/// Seeds the neighbours, writes an int[] to a 4-element Int array, reads it all back, restores.
/// The simulator profile.
/// The transport stack the driver runs on.
[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");
}
}
}