diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyArrayWriteIntegrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyArrayWriteIntegrationTests.cs
index 0b479e73..9a098667 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyArrayWriteIntegrationTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyArrayWriteIntegrationTests.cs
@@ -6,16 +6,21 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
///
-/// Review 07 U10: an N-file array write (N7:2, 4 elements) round-trips every element against
-/// ab_server on both transports, and leaves the words either side untouched. Simulator-only (gated by
-/// ); the real-controller proof is a hardware userGate.
+/// 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)
{
- /// Seeds the neighbours, writes an int[] to a 4-element Int array, reads it all back.
+ /// 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]
@@ -25,20 +30,29 @@ public sealed class AbLegacyArrayWriteIntegrationTests(AbLegacyServerFixture sim
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(
- new AbLegacyTagDefinition("Before", deviceUri, "N7:1", AbLegacyDataType.Int),
- new AbLegacyTagDefinition("Arr", deviceUri, "N7:2", AbLegacyDataType.Int, ArrayLength: 4),
- new AbLegacyTagDefinition("After", deviceUri, "N7:6", AbLegacyDataType.Int)),
+ [.. 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)
{
@@ -48,23 +62,34 @@ public sealed class AbLegacyArrayWriteIntegrationTests(AbLegacyServerFixture sim
return;
}
- (await drv.WriteAsync(
- [new WriteRequest("Before", (short)111), new WriteRequest("After", (short)666)], ct))
- .ShouldAllBe(r => r.StatusCode == AbLegacyStatusMapper.Good);
+ 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);
+ // 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 back = await drv.ReadAsync(["Before", "Arr", "After"], ct);
- back.ShouldAllBe(s => s.StatusCode == AbLegacyStatusMapper.Good);
- back[1].Value.ShouldBe(new short[] { 10, -20, 30, 32767 });
- Convert.ToInt32(back[0].Value).ShouldBe(111); // neighbours untouched
- Convert.ToInt32(back[2].Value).ShouldBe(666);
+ 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 });
+ // 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");
+ }
}
}