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; /// /// End-to-end smoke tests against the ab_server PCCC Docker container. /// Promotes the AB Legacy driver from unit-only coverage (FakeAbLegacyTag) /// to wire-level: real PCCC stack (libplctag and native, via ) over real TCP against the ab_server /// simulator. Parametrised over all three families (SLC 500 / MicroLogix / PLC-5) /// via [AbLegacyTheory] + [MemberData]. /// [Collection(AbLegacyServerCollection.Name)] [Trait("Category", "Integration")] [Trait("Simulator", "ab_server-PCCC")] public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim) { // Rows come from TransportMatrix.Profiles, which keeps the AB_LEGACY_COMPOSE_PROFILE filter this // class applied before the transport split: when set, only that profile is crossed with the // transports, so the suite matches the running container. /// Verifies that the driver reads seeded N file from the AB server via PCCC. /// The AB Legacy server profile describing the fixture endpoint. /// The transport stack the driver runs on. [AbLegacyTheory] [MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))] public async Task Driver_reads_seeded_N_file_from_ab_server_PCCC(AbLegacyServerProfile profile, AbLegacyTransport transport) { if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip); // PCCC semantics allow an empty cip-path (real SLC/PLC-5 hardware takes nothing // after the `/`), but libplctag's ab_server requires a non-empty path at the // CIP unconnected-send layer before the PCCC dispatcher runs. Default `1,0` // against the Docker fixture; set AB_LEGACY_CIP_PATH= (empty) against real HW. var deviceUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}"; await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(deviceUri, profile.Family)], RawTags = AbLegacyRawTags.Entries( new AbLegacyTagDefinition( Name: "IntCounter", DeviceHostAddress: deviceUri, Address: "N7:0", DataType: AbLegacyDataType.Int)), Timeout = TimeSpan.FromSeconds(5), Probe = new AbLegacyProbeOptions { Enabled = false }, Transport = transport, }, driverInstanceId: $"ablegacy-smoke-{profile.Family}-{transport}"); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); var snapshots = await drv.ReadAsync( ["IntCounter"], TestContext.Current.CancellationToken); snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good, $"N7:0 read must succeed against the {profile.Family} compose profile"); drv.GetHealth().State.ShouldBe(DriverState.Healthy); } /// Verifies that SLC500 write-then-read round trip succeeds on N7 scratch register. /// The transport stack the driver runs on. [AbLegacyTheory] [MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))] public async Task Slc500_write_then_read_round_trip_on_N7_scratch_register(AbLegacyTransport transport) { // Addresses the SLC500 container by its OWN port, so it no longer matters which other // families happen to be running. The old AB_LEGACY_COMPOSE_PROFILE guard existed because every // family shared :44818 and this test could otherwise write SLC500 semantics at whichever // container held the port; it survives below only for real-hardware runs, where // AB_LEGACY_ENDPOINT pins one device and the family IS a property of that device. if (sim.SkipReasonFor(KnownProfiles.Slc500) is { } skip) Assert.Skip(skip); IntegrationWriteGate.SkipUnlessWritesAllowed(); // writes N7:5 var only = Environment.GetEnvironmentVariable("AB_LEGACY_COMPOSE_PROFILE"); if (sim.EndpointPinned && !string.IsNullOrEmpty(only) && !string.Equals(only, "slc500", StringComparison.OrdinalIgnoreCase)) { Assert.Skip($"Pinned endpoint is not SLC500; AB_LEGACY_COMPOSE_PROFILE='{only}'."); } // PCCC semantics allow an empty cip-path (real SLC/PLC-5 hardware takes nothing // after the `/`), but libplctag's ab_server requires a non-empty path at the // CIP unconnected-send layer before the PCCC dispatcher runs. Default `1,0` // against the Docker fixture; set AB_LEGACY_CIP_PATH= (empty) against real HW. var deviceUri = $"ab://{sim.Host}:{sim.PortFor(KnownProfiles.Slc500)}/{sim.CipPath}"; await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(deviceUri, AbLegacyPlcFamily.Slc500)], RawTags = AbLegacyRawTags.Entries( new AbLegacyTagDefinition( Name: "Scratch", DeviceHostAddress: deviceUri, Address: "N7:5", DataType: AbLegacyDataType.Int, Writable: true)), Timeout = TimeSpan.FromSeconds(5), Probe = new AbLegacyProbeOptions { Enabled = false }, Transport = transport, }, driverInstanceId: $"ablegacy-smoke-rw-{transport}"); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); const short probe = 0x1234; var writeResults = await drv.WriteAsync( [new WriteRequest("Scratch", probe)], TestContext.Current.CancellationToken); writeResults.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "PCCC N7:5 write must succeed end-to-end"); var readResults = await drv.ReadAsync( ["Scratch"], TestContext.Current.CancellationToken); readResults.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); Convert.ToInt32(readResults.Single().Value).ShouldBe(probe); } }