using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests; /// /// End-to-end smoke tests that exercise the real transport stack (libplctag and native, via ) against a running /// ab_server. Skipped when the binary isn't on PATH (). /// Parametrized over so one test file covers every family /// (ControlLogix / CompactLogix / Micro800 / GuardLogix). /// [Trait("Category", "Integration")] [Trait("Requires", "AbServer")] public sealed class AbCipReadSmokeTests { /// Verifies that the driver can read a seeded DInt value from an AB server. /// The AB server profile to test against. /// The transport stack the driver runs on. [AbServerTheory] [MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))] public async Task Driver_reads_seeded_DInt_from_ab_server(AbServerProfile profile, AbCipTransport transport) { var fixture = new AbServerFixture(profile); await fixture.InitializeAsync(); try { // Use fixture.Host (not hardcoded 127.0.0.1) so the docker-host migration // (10.100.0.35) and AB_SERVER_ENDPOINT overrides reach this test too. The // sibling Emulate tests already use the fixture's resolved endpoint; this // smoke test was missed. // Micro800 has no backplane: its cip-path is empty (the compose service runs // ab_server --plc=Micro800 with no --path, which refuses a routed request). Every // other family reaches slot 0 through `1,0`. Both transports are expected to pass // for Micro800: the libplctag adapters map the empty route to a null `path` attribute // (LibplctagPath.Normalize) because libplctag rejects "" with ErrorBadData. var cipPath = profile.Family == AbCipPlcFamily.Micro800 ? "" : "1,0"; var deviceUri = $"ab://{fixture.Host}:{fixture.Port}/{cipPath}"; var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)], RawTags = AbCipRawTags.From(new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)), Timeout = TimeSpan.FromSeconds(5), Transport = transport, }, $"drv-smoke-{profile.Family}-{transport}"); await drv.InitializeAsync("{}", CancellationToken.None); var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None); snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good); drv.GetHealth().State.ShouldBe(DriverState.Healthy); await drv.ShutdownAsync(CancellationToken.None); } finally { await fixture.DisposeAsync(); } } }