using System.Net.Sockets; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests; /// /// Reachability probe for a Modbus TCP simulator (pymodbus-driven, see /// Pymodbus/serve.ps1) or a real PLC. Parses /// MODBUS_SIM_ENDPOINT (default localhost:5020 per PR 43) and TCP-connects once at /// fixture construction. Each test checks and calls /// Assert.Skip when the endpoint was unreachable, so a dev box without a running /// simulator still passes `dotnet test` cleanly — matches the Galaxy live-smoke pattern in /// GalaxyRepositoryLiveSmokeTests. /// /// /// /// Do NOT keep the probe socket open for the life of the fixture. The probe is a /// one-shot liveness check; tests open their own transports (the real /// ) against the same endpoint. Sharing a socket /// across tests would serialize them on a single TCP stream. /// /// /// The fixture is a collection fixture so the reachability probe runs once per test /// session, not per test — checking every test would waste several seconds against a /// firewalled endpoint that times out each attempt. /// /// public sealed class ModbusSimulatorFixture : IAsyncDisposable { // PR 43: default port is 5020 (pymodbus convention) instead of 502 (Modbus standard). // Picking 5020 sidesteps the privileged-port admin requirement on Windows + matches the // port baked into the pymodbus simulator JSON profiles in Pymodbus/. Override with // MODBUS_SIM_ENDPOINT to point at a real PLC on its native port 502. private const string DefaultEndpoint = "localhost:5020"; private const string EndpointEnvVar = "MODBUS_SIM_ENDPOINT"; public string Host { get; } public int Port { get; } public string? SkipReason { get; } public ModbusSimulatorFixture() { var raw = Environment.GetEnvironmentVariable(EndpointEnvVar) ?? DefaultEndpoint; var parts = raw.Split(':', 2); Host = parts[0]; Port = parts.Length == 2 && int.TryParse(parts[1], out var p) ? p : 502; try { using var client = new TcpClient(); var task = client.ConnectAsync(Host, Port); if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected) { SkipReason = $"Modbus simulator at {Host}:{Port} did not accept a TCP connection within 2s. " + $"Start the pymodbus simulator (Pymodbus\\serve.ps1 -Profile standard) " + $"or override {EndpointEnvVar}, then re-run."; } } catch (Exception ex) { SkipReason = $"Modbus simulator at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " + $"Start the pymodbus simulator (Pymodbus\\serve.ps1 -Profile standard) " + $"or override {EndpointEnvVar}, then re-run."; } } public ValueTask DisposeAsync() => ValueTask.CompletedTask; } [Xunit.CollectionDefinition(Name)] public sealed class ModbusSimulatorCollection : Xunit.ICollectionFixture { public const string Name = "ModbusSimulator"; }