Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipReadWidthMismatchTests.cs
T
Joseph Doherty e3d6b76ede fix(abcip): a scalar read whose width disagrees with its authored type is BadTypeMismatch
libplctag decodes whatever bytes the controller returns as the authored type and never
checks the CIP type code. Live read-only check on a production press: WW_ActualVacuum is a
REAL (L5X + the controller's symbol table agree) but the legacy Kepware export in the
ignitionoee repo types it Short; read as INT it returned Good -17346 while the real value
was 5.37. A scalar atomic read (not an array, not a bit-in-DINT) whose buffer size is not
the authored type's width now reads BadTypeMismatch with a warning naming both. Widths
observed live: SINT/BOOL 1, INT 2, DINT/REAL 4, LINT 8. IAbCipTagRuntime.GetSize() is a
default-interface member (-1 = unknown, never checked). Same-width mismatches (DINT vs
REAL) stay undetectable without the type code libplctag strips.
2026-10-07 14:15:19 -04:00

73 lines
3.2 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
/// <summary>
/// libplctag hands back whatever bytes the controller sends and decodes them as the authored type
/// — it never checks the CIP type code. Seen live on a production press: <c>WW_ActualVacuum</c> is a
/// REAL (the L5X and the controller's symbol table agree), the legacy Kepware export types it as
/// Short, and reading it authored as INT returned <c>Good -17346</c> (the low half of a float)
/// where the real value was 5.37. A scalar atomic read whose buffer is not the authored type's
/// width is now <c>BadTypeMismatch</c>. Same-width mismatches (DINT vs REAL) remain undetectable
/// without the type code.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipReadWidthMismatchTests
{
private const string Device = "ab://10.0.0.5/1,0";
private static async Task<uint> ReadStatusAsync(AbCipDataType authored, int bufferSize, string path = "WW_ActualVacuum")
{
var factory = new FakeAbCipTagFactory
{
Customise = p => new FakeAbCipTag(p) { Value = 1, Size = bufferSize },
};
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
RawTags = AbCipRawTags.From(new AbCipTagDefinition("T", Device, path, authored)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "abcip-1", factory);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var r = await drv.ReadAsync(["T"], TestContext.Current.CancellationToken);
return r[0].StatusCode;
}
/// <summary>An INT read that comes back 4 bytes wide is a type mismatch, not a Good value.</summary>
[Fact]
public async Task INT_authored_over_a_4_byte_REAL_is_BadTypeMismatch()
{
(await ReadStatusAsync(AbCipDataType.Int, bufferSize: 4)).ShouldBe(AbCipStatusMapper.BadTypeMismatch);
}
/// <summary>Matching widths still read Good — including the live-observed sizes per type.</summary>
/// <param name="type">Authored type.</param>
/// <param name="size">Buffer size the controller returned for that type (observed live).</param>
[Theory]
[InlineData(AbCipDataType.Int, 2)]
[InlineData(AbCipDataType.DInt, 4)]
[InlineData(AbCipDataType.Real, 4)]
[InlineData(AbCipDataType.LInt, 8)]
[InlineData(AbCipDataType.SInt, 1)]
public async Task Matching_width_reads_Good(AbCipDataType type, int size)
{
(await ReadStatusAsync(type, size)).ShouldBe(AbCipStatusMapper.Good);
}
/// <summary>An unknown size (a runtime that cannot report it) never blocks a read.</summary>
[Fact]
public async Task Unknown_size_is_not_checked()
{
(await ReadStatusAsync(AbCipDataType.Int, bufferSize: -1)).ShouldBe(AbCipStatusMapper.Good);
}
/// <summary>A bit-in-DINT read decodes from the parent's 4-byte buffer and is not width-checked.</summary>
[Fact]
public async Task Bit_index_read_is_not_width_checked()
{
(await ReadStatusAsync(AbCipDataType.Bool, bufferSize: 4, path: "Flags.3")).ShouldBe(AbCipStatusMapper.Good);
}
}