using System.Net.Sockets; using Xunit; using Xunit.Sdk; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests; /// /// Reachability probe for the ab_server Docker container (libplctag's CIP /// simulator built via Docker/Dockerfile) or any real AB PLC the /// AB_SERVER_ENDPOINT env var points at. Parses /// AB_SERVER_ENDPOINT (default localhost:44818) + TCP-connects /// once at fixture construction. Tests skip via /// / when the port isn't live, so /// dotnet test stays green on a fresh clone without Docker running. /// Matches the / Snap7ServerFixture / /// OpcPlcFixture shape. /// /// /// Docker is the only supported launch path — no native-binary spawn + no /// PATH lookup. Bring the container up before dotnet test: /// docker compose -f Docker/docker-compose.yml --profile controllogix up. /// public sealed class AbServerFixture : IAsyncLifetime { private const string EndpointEnvVar = "AB_SERVER_ENDPOINT"; /// The profile this fixture instance represents. Parallel family smoke tests /// instantiate the fixture with the profile matching their compose-file service. public AbServerProfile Profile { get; } public string Host { get; } = "127.0.0.1"; public int Port { get; } = AbServerProfile.DefaultPort; public AbServerFixture() : this(KnownProfiles.ControlLogix) { } public AbServerFixture(AbServerProfile profile) { Profile = profile ?? throw new ArgumentNullException(nameof(profile)); // Endpoint override applies to both host + port — targeting a real PLC at // non-default host or port shouldn't need fixture changes. if (Environment.GetEnvironmentVariable(EndpointEnvVar) is { Length: > 0 } raw) { var parts = raw.Split(':', 2); Host = parts[0]; if (parts.Length == 2 && int.TryParse(parts[1], out var p)) Port = p; } } public ValueTask InitializeAsync() => ValueTask.CompletedTask; public ValueTask DisposeAsync() => ValueTask.CompletedTask; /// /// true when ab_server is reachable at this fixture's Host/Port. Used by /// / /// to decide whether to skip tests on a fresh clone without a running container. /// public static bool IsServerAvailable() => TcpProbe(ResolveHost(), ResolvePort()); private static string ResolveHost() => Environment.GetEnvironmentVariable(EndpointEnvVar)?.Split(':', 2)[0] ?? "127.0.0.1"; private static int ResolvePort() { var raw = Environment.GetEnvironmentVariable(EndpointEnvVar); if (raw is null) return AbServerProfile.DefaultPort; var parts = raw.Split(':', 2); return parts.Length == 2 && int.TryParse(parts[1], out var p) ? p : AbServerProfile.DefaultPort; } /// One-shot TCP probe; 500 ms budget so a missing container fails the probe fast. private static bool TcpProbe(string host, int port) { try { using var client = new TcpClient(); var task = client.ConnectAsync(host, port); return task.Wait(TimeSpan.FromMilliseconds(500)) && client.Connected; } catch { return false; } } } /// /// [Fact]-equivalent that skips when ab_server isn't reachable — accepts a /// live Docker listener on localhost:44818 or an AB_SERVER_ENDPOINT /// override pointing at a real PLC. /// public sealed class AbServerFactAttribute : FactAttribute { public AbServerFactAttribute() { if (!AbServerFixture.IsServerAvailable()) Skip = "ab_server not reachable. Start the Docker container " + "(docker compose -f Docker/docker-compose.yml --profile controllogix up) " + "or set AB_SERVER_ENDPOINT to a real PLC."; } } /// /// [Theory]-equivalent with the same availability rules as /// . Pair with /// [MemberData(nameof(KnownProfiles.All))]-style providers to run one theory /// row per family. /// public sealed class AbServerTheoryAttribute : TheoryAttribute { public AbServerTheoryAttribute() { if (!AbServerFixture.IsServerAvailable()) Skip = "ab_server not reachable. Start the Docker container " + "(docker compose -f Docker/docker-compose.yml --profile controllogix up) " + "or set AB_SERVER_ENDPOINT to a real PLC."; } }