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