e3d6b76ede
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.
73 lines
3.2 KiB
C#
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);
|
|
}
|
|
}
|