abbaac3615
R07:L5 (AB half): CipWriter rents its working buffer from ArrayPool and returns it in ToArray (which now completes the writer; later writes throw). A single-reply Logix read is one copy of the data (no List<byte> + ToArray); a fragmented read reassembles in a pooled buffer. EncapsulationClient reads the frame header into a per-session buffer (reads only happen under the session gate) instead of a new array per reply. Measured per op (scratch A/B, Release): frame encode 784 -> 200 B, Logix read 1994 -> 1655 B, 3-fragment read 11.9 -> 8.2 KB, EIP round trip 2.75 -> 2.15 KB; encode time 41-46 -> 28-39 ms per 500k, other timings within noise; Ab.Transport.Tests 3 s before and after. Modbus transports are left to a follow-up (its lane is still in review). Refs #747 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
85 lines
3.0 KiB
C#
85 lines
3.0 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire;
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Wire;
|
|
|
|
public sealed class CipWriterTests
|
|
{
|
|
[Fact]
|
|
public void PatchU16_overwrites_an_already_written_word()
|
|
{
|
|
var w = new CipWriter().U16(0).U8(7);
|
|
w.PatchU16(0, 0xABCD);
|
|
w.ToArray().ShouldBe(new byte[] { 0xCD, 0xAB, 0x07 });
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(2)] // would straddle the end: only bytes 0..2 are written
|
|
[InlineData(3)]
|
|
[InlineData(100)] // inside spare buffer capacity (256), but never written
|
|
public void PatchU16_outside_the_written_range_throws(int offset)
|
|
{
|
|
var w = new CipWriter().U16(0).U8(7);
|
|
Should.Throw<ArgumentOutOfRangeException>(() => w.PatchU16(offset, 1));
|
|
w.ToArray().ShouldBe(new byte[] { 0, 0, 7 }); // nothing was patched
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// R07:L5: <see cref="CipWriter"/> works in a buffer rented from <see cref="System.Buffers.ArrayPool{T}"/> and hands
|
|
/// it back on <see cref="CipWriter.ToArray"/>. The returned array must be the caller's own (a later writer reusing
|
|
/// the pooled buffer cannot change it), growth must keep every byte, and a writer used after ToArray must refuse
|
|
/// rather than write into a buffer it no longer owns.
|
|
/// </summary>
|
|
public sealed class CipWriterPoolingTests
|
|
{
|
|
[Fact]
|
|
public void Growth_past_the_initial_buffer_keeps_every_byte()
|
|
{
|
|
var w = new CipWriter();
|
|
for (var i = 0; i < 1000; i++) w.U8((byte)i);
|
|
var bytes = w.ToArray();
|
|
bytes.Length.ShouldBe(1000);
|
|
bytes.Select((b, i) => b == (byte)i).ShouldAllBe(ok => ok);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_returned_array_is_not_affected_by_a_later_writer_reusing_the_pooled_buffer()
|
|
{
|
|
var first = new CipWriter().U32(0xAABBCCDD).ToArray();
|
|
for (var i = 0; i < 50; i++) new CipWriter().U32(0x11111111).U32(0x22222222).ToArray();
|
|
first.ShouldBe(new byte[] { 0xDD, 0xCC, 0xBB, 0xAA });
|
|
}
|
|
|
|
[Fact]
|
|
public void ToArray_twice_returns_equal_independent_copies()
|
|
{
|
|
var w = new CipWriter().U16(0x0102);
|
|
var a = w.ToArray();
|
|
var b = w.ToArray();
|
|
b.ShouldBe(a);
|
|
b.ShouldNotBeSameAs(a);
|
|
}
|
|
|
|
[Fact]
|
|
public void Writing_after_ToArray_is_refused()
|
|
{
|
|
var w = new CipWriter().U8(1);
|
|
_ = w.ToArray();
|
|
Should.Throw<InvalidOperationException>(() => w.U8(2));
|
|
Should.Throw<InvalidOperationException>(() => w.PatchU16(0, 1));
|
|
w.Length.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Nested_writers_produce_independent_frames()
|
|
{
|
|
// The Execute PCCC / encapsulation builders build an inner writer while an outer one is live.
|
|
var outer = new CipWriter().U8(0xAA);
|
|
var inner = new CipWriter().U8(0x01).U8(0x02).ToArray();
|
|
var frame = outer.Bytes(inner).U8(0xBB).ToArray();
|
|
frame.ShouldBe(new byte[] { 0xAA, 0x01, 0x02, 0xBB });
|
|
}
|
|
}
|