ModbusSimulatorFixture is a collection fixture so the 2s TCP probe runs once per run, not per test; SkipReason gets a clear operator-facing message ('start ModbusPal or override MODBUS_SIM_ENDPOINT'). Tests call Assert.Skip(sim.SkipReason) rather than silently returning — matches the test-plan convention and reads cleanly in CI logs. DL205Profile.BuildOptions deliberately disables the background probe loop since integration tests drive reads explicitly and the probe would race with assertions. Tag naming uses the DL205_ prefix so filter 'DisplayName~DL205' surfaces device-specific failures at a glance.
Project references: xunit.v3 + Shouldly + Microsoft.NET.Test.Sdk + xunit.runner.visualstudio (matches the existing Driver.Modbus.Tests unit project), project ref to src/Driver.Modbus. Registered in ZB.MOM.WW.OtOpcUa.slnx under tests/. ModbusPal/README.md documents the dev loop (install ModbusPal jar, load profile, start simulator, dotnet test), explains MODBUS_SIM_ENDPOINT override for real-PLC benchwork, and flags DL205.xmpp as the first profile to add in a follow-up PR.
dotnet test run against the scaffold (no simulator running) skips cleanly: 0 failed, 0 passed, 1 skipped, with the SkipReason surfaced. dotnet build clean (0 warnings, 0 errors). Updated docs/v2/modbus-test-plan.md to mark the scaffold PR done and renumbered future PRs from 'PR 27+' to 'PR 31+' to stay in sync with the actual PR chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.7 KiB
C#
67 lines
2.7 KiB
C#
using System.Net.Sockets;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Reachability probe for a Modbus TCP simulator (ModbusPal or a real PLC). Parses
|
|
/// <c>MODBUS_SIM_ENDPOINT</c> (default <c>localhost:502</c>) and TCP-connects once at
|
|
/// fixture construction. Each test checks <see cref="SkipReason"/> and calls
|
|
/// <c>Assert.Skip</c> 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
|
|
/// <c>GalaxyRepositoryLiveSmokeTests</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// 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
|
|
/// <see cref="ModbusTcpTransport"/>) against the same endpoint. Sharing a socket
|
|
/// across tests would serialize them on a single TCP stream.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class ModbusSimulatorFixture : IAsyncDisposable
|
|
{
|
|
private const string DefaultEndpoint = "localhost:502";
|
|
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 ModbusPal (or override {EndpointEnvVar}) and re-run.";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
SkipReason = $"Modbus simulator at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
|
|
$"Start ModbusPal (or override {EndpointEnvVar}) and re-run.";
|
|
}
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
}
|
|
|
|
[Xunit.CollectionDefinition(Name)]
|
|
public sealed class ModbusSimulatorCollection : Xunit.ICollectionFixture<ModbusSimulatorFixture>
|
|
{
|
|
public const string Name = "ModbusSimulator";
|
|
}
|