7e958cd750
R07:L5 (Modbus half, former F2): ModbusTcpTransport builds the MBAP ADU in a pooled buffer and reads the reply header into one per-transport buffer (sends are single-flight under the gate). ModbusRtuOverTcpTransport writes its ADU into a pooled buffer (ModbusRtuFraming.WriteAdu, byte-identical to BuildAdu), and ReadResponsePduAsync reads and validates the whole frame in one pooled buffer instead of five arrays. Only the returned PDU is allocated. The exchange stays one async method: splitting it cost a state-machine box that ate the saving. Scratch A/B per op: RTU deframe 352 -> 184 B, RTU send 640 -> 408 B, TCP send 864 -> 800 B; timings within noise; Driver.Modbus.Tests 2 s before and after. NormalizeWordOrder lives in ModbusDriver (not a transport) and is left alone. Refs #747 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
63 lines
2.9 KiB
C#
63 lines
2.9 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
|
|
|
/// <summary>
|
|
/// R07:L5 (Modbus half): the RTU ADU is now written into a pooled buffer and the response is deframed in one pooled
|
|
/// buffer. The wire bytes and the decoded PDUs must be exactly what the allocating code produced, and a reused pooled
|
|
/// buffer must never leak a previous frame's bytes into the next result.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ModbusPooledFramingTests
|
|
{
|
|
/// <summary>The pooled ADU writer matches the reference construction (unit + PDU, CRC appended low byte first).</summary>
|
|
/// <param name="hex">The PDU as hex.</param>
|
|
[Theory]
|
|
[InlineData("0300000001")]
|
|
[InlineData("10000A0002040001000200")]
|
|
[InlineData("06")]
|
|
public void WriteAdu_matches_the_reference_ADU(string hex)
|
|
{
|
|
var pdu = Convert.FromHexString(hex);
|
|
var reference = ModbusCrc.Append([0x11, .. pdu]);
|
|
|
|
var pooled = new byte[ModbusRtuFraming.AduLength(pdu.Length) + 7]; // oversized, like a rented array
|
|
ModbusRtuFraming.WriteAdu(0x11, pdu, pooled.AsSpan(0, ModbusRtuFraming.AduLength(pdu.Length)));
|
|
|
|
pooled.AsSpan(0, reference.Length).ToArray().ShouldBe(reference);
|
|
ModbusRtuFraming.BuildAdu(0x11, pdu).ShouldBe(reference);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A long read reply followed by a short echo and an exception frame, each deframed through the pooled buffer:
|
|
/// every result is exactly its own PDU (no stale tail from the previous, longer frame).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Consecutive_deframes_through_the_pooled_buffer_do_not_leak_bytes()
|
|
{
|
|
var ct = TestContext.Current.CancellationToken;
|
|
byte[] longRead = [0x03, 200, .. Enumerable.Range(0, 200).Select(i => (byte)(i + 1))];
|
|
byte[] echo = [0x06, 0x00, 0x01, 0x00, 0x2A];
|
|
|
|
(await ModbusRtuFraming.ReadResponsePduAsync(new MemoryStream(ModbusRtuFraming.BuildAdu(1, longRead)), 1, ct))
|
|
.ShouldBe(longRead);
|
|
(await ModbusRtuFraming.ReadResponsePduAsync(new MemoryStream(ModbusRtuFraming.BuildAdu(1, echo)), 1, ct))
|
|
.ShouldBe(echo);
|
|
var ex = await Should.ThrowAsync<ModbusException>(() =>
|
|
ModbusRtuFraming.ReadResponsePduAsync(new MemoryStream(ModbusRtuFraming.BuildAdu(1, [0x83, 0x02])), 1, ct));
|
|
ex.ExceptionCode.ShouldBe((byte)0x02);
|
|
(await ModbusRtuFraming.ReadResponsePduAsync(new MemoryStream(ModbusRtuFraming.BuildAdu(1, [0x03, 2, 9, 8])), 1, ct))
|
|
.ShouldBe(new byte[] { 0x03, 2, 9, 8 });
|
|
}
|
|
|
|
/// <summary>A byte-count-255 read (the largest RTU reply) fits the pooled frame.</summary>
|
|
[Fact]
|
|
public async Task The_largest_read_reply_deframes()
|
|
{
|
|
byte[] max = [0x04, 255, .. new byte[255]];
|
|
(await ModbusRtuFraming.ReadResponsePduAsync(new MemoryStream(ModbusRtuFraming.BuildAdu(1, max)), 1,
|
|
TestContext.Current.CancellationToken)).ShouldBe(max);
|
|
}
|
|
}
|