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"); + } } }