56eee3c563
Adds the mbproxy service end-to-end. Phases 00-08 implement the production-ready single-listener / 1:1-backend transparent Modbus TCP proxy with bidirectional BCD rewriting for the ~54-PLC DL205/DL260 fleet. Phase 9 replaces the connection layer with a single backend socket per PLC plus MBAP TxId rewriting, lifting the H2-ECOM100's 4-concurrent-client cap as an operational ceiling. Phase 9 additions of note: - PlcMultiplexer + UpstreamPipe + TxIdAllocator + CorrelationMap - InFlightRequest with IReadOnlyList<InterestedParty> (load-bearing for Phase 10 read coalescing — do not collapse to a single field) - Per-request watchdog: surfaces Modbus exception 0x0B to upstream on BackendRequestTimeoutMs, defending against lost responses, dead-PLC paths, and pymodbus 3.13.0's concurrent-multiplexed- request bug (its ServerRequestHandler.last_pdu state race) - Status DTO + HTML gain inFlight / maxInFlight / txIdWraps / disconnectCascades / queueDepth (Tier 1.6 in docs/kpi.md) Tests: 263 unit + 38 E2E. Multiplexer correctness under truly concurrent backend traffic is proved against a stub backend in PlcMultiplexerTests; MultiplexerE2ETests paces requests so pymodbus 3.13's single-PDU framer stays in known-good mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.3 KiB
C#
92 lines
3.3 KiB
C#
using System.Net.Sockets;
|
|
using NModbus;
|
|
using Xunit;
|
|
|
|
namespace Mbproxy.Tests.Sim;
|
|
|
|
/// <summary>
|
|
/// End-to-end smoke tests that verify the pymodbus DL205 simulator is reachable and
|
|
/// serves the expected seeded register values from <c>DL260/dl205.json</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// All three tests call <see cref="Assert.Skip"/> when
|
|
/// <see cref="DL205SimulatorFixture.SkipReason"/> is non-null (Python or pymodbus
|
|
/// unavailable). This is the expected "green" outcome on machines without Python.
|
|
/// </remarks>
|
|
[Collection(nameof(DL205SimulatorCollection))]
|
|
[Trait("Category", "E2E")]
|
|
public sealed class SimulatorSmokeTests
|
|
{
|
|
private readonly DL205SimulatorFixture _sim;
|
|
|
|
public SimulatorSmokeTests(DL205SimulatorFixture sim)
|
|
{
|
|
_sim = sim;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the simulator process is running and accepts a plain TCP
|
|
/// connection on its allocated port.
|
|
/// </summary>
|
|
[Fact(Timeout = 5_000)]
|
|
public async Task Simulator_AcceptsTcpConnection()
|
|
{
|
|
if (_sim.SkipReason is not null)
|
|
Assert.Skip(_sim.SkipReason);
|
|
|
|
using var client = new TcpClient();
|
|
await client.ConnectAsync(_sim.Host, _sim.Port, TestContext.Current.CancellationToken);
|
|
|
|
Assert.True(client.Connected,
|
|
"TcpClient should be connected to the simulator.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads holding register 0 via FC03 and expects the DL205 marker value
|
|
/// <c>0xCAFE</c> (51966 decimal). This proves that the dl205.json profile is
|
|
/// actually loaded — a bare pymodbus server with no profile returns 0.
|
|
/// </summary>
|
|
[Fact(Timeout = 5_000)]
|
|
public async Task Simulator_FC03_ReturnsSeededValue_AtHR0_0xCAFE()
|
|
{
|
|
if (_sim.SkipReason is not null)
|
|
Assert.Skip(_sim.SkipReason);
|
|
|
|
using var client = new TcpClient();
|
|
await client.ConnectAsync(_sim.Host, _sim.Port, TestContext.Current.CancellationToken);
|
|
|
|
var factory = new ModbusFactory();
|
|
var master = factory.CreateMaster(client);
|
|
|
|
// FC03: read 1 holding register at address 0, unit ID 1.
|
|
ushort[] registers = master.ReadHoldingRegisters(slaveAddress: 1, startAddress: 0, numberOfPoints: 1);
|
|
|
|
Assert.Equal(0xCAFE, registers[0]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads holding register 1072 via FC03 and expects raw BCD value
|
|
/// <c>0x1234</c> (4660 decimal). This register represents decimal 1234 stored as
|
|
/// BCD nibbles. Phase 04's e2e test will read the same register through the proxy
|
|
/// and assert binary 1234 — proving the proxy rewrote the response.
|
|
/// </summary>
|
|
[Fact(Timeout = 5_000)]
|
|
public async Task Simulator_FC03_ReturnsBCD_RawValueAtHR1072_0x1234()
|
|
{
|
|
if (_sim.SkipReason is not null)
|
|
Assert.Skip(_sim.SkipReason);
|
|
|
|
using var client = new TcpClient();
|
|
await client.ConnectAsync(_sim.Host, _sim.Port, TestContext.Current.CancellationToken);
|
|
|
|
var factory = new ModbusFactory();
|
|
var master = factory.CreateMaster(client);
|
|
|
|
// FC03: read 1 holding register at address 1072, unit ID 1.
|
|
// dl205.json seeds: addr 1072, value 4660 (= 0x1234).
|
|
ushort[] registers = master.ReadHoldingRegisters(slaveAddress: 1, startAddress: 1072, numberOfPoints: 1);
|
|
|
|
Assert.Equal(0x1234, registers[0]); // raw BCD nibbles, NOT binary 1234
|
|
}
|
|
}
|