using System.Net.Sockets;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests;
///
/// Reachability probe for a python-snap7 simulator (see
/// PythonSnap7/serve.ps1) or a real S7 PLC. Parses S7_SIM_ENDPOINT
/// (default localhost:1102) + TCP-connects once at fixture construction.
/// Tests check + call Assert.Skip when unreachable, so
/// `dotnet test` stays green on a fresh box without the simulator installed —
/// mirrors the ModbusSimulatorFixture pattern.
///
///
///
/// Default port is 1102, not the S7-standard 102. 102 is a privileged port
/// on Linux (needs root) + triggers the Windows Firewall prompt on first bind;
/// 1102 sidesteps both. S7netplus 0.20 supports the 5-arg Plc ctor that
/// takes an explicit port (verified + wired through S7DriverOptions.Port),
/// so the driver can reach the simulator on its non-standard port without
/// hacks.
///
///
/// The probe is a one-shot liveness check; tests open their own S7netplus
/// sessions against the same endpoint. Don't share a socket — S7 CPUs serialise
/// concurrent connections against the same mailbox anyway, but sharing would
/// couple test ordering to socket reuse in ways this harness shouldn't care
/// about.
///
///
/// Fixture is a collection fixture so the probe runs once per test session, not
/// per test.
///
///
public sealed class Snap7ServerFixture : IAsyncDisposable
{
// Default 1102 (non-privileged) matches PythonSnap7/server.py. Override with
// S7_SIM_ENDPOINT to point at a real PLC on its native 102.
private const string DefaultEndpoint = "localhost:1102";
private const string EndpointEnvVar = "S7_SIM_ENDPOINT";
public string Host { get; }
public int Port { get; }
public string? SkipReason { get; }
public Snap7ServerFixture()
{
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 : 102;
try
{
// Force IPv4 — python-snap7 binds 0.0.0.0 (IPv4) and .NET's default
// dual-stack "localhost" resolves IPv6 ::1 first then times out before
// falling back. Same story the Modbus fixture hits.
using var client = new TcpClient(AddressFamily.InterNetwork);
var task = client.ConnectAsync(
System.Net.Dns.GetHostAddresses(Host)
.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork)
?? System.Net.IPAddress.Loopback,
Port);
if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected)
{
SkipReason = $"python-snap7 simulator at {Host}:{Port} did not accept a TCP connection within 2s. " +
$"Start it (PythonSnap7\\serve.ps1 -Profile s7_1500) or override {EndpointEnvVar}.";
}
}
catch (Exception ex)
{
SkipReason = $"python-snap7 simulator at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
$"Start it (PythonSnap7\\serve.ps1 -Profile s7_1500) or override {EndpointEnvVar}.";
}
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
[Xunit.CollectionDefinition(Name)]
public sealed class Snap7ServerCollection : Xunit.ICollectionFixture
{
public const string Name = "Snap7Server";
}