using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// 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: WW_ActualVacuum 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 Good -17346 (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 BadTypeMismatch. Same-width mismatches (DINT vs REAL) remain undetectable /// without the type code. /// [Trait("Category", "Unit")] public sealed class AbCipReadWidthMismatchTests { private const string Device = "ab://10.0.0.5/1,0"; private static async Task 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; } /// An INT read that comes back 4 bytes wide is a type mismatch, not a Good value. [Fact] public async Task INT_authored_over_a_4_byte_REAL_is_BadTypeMismatch() { (await ReadStatusAsync(AbCipDataType.Int, bufferSize: 4)).ShouldBe(AbCipStatusMapper.BadTypeMismatch); } /// Matching widths still read Good — including the live-observed sizes per type. /// Authored type. /// Buffer size the controller returned for that type (observed live). [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); } /// An unknown size (a runtime that cannot report it) never blocks a read. [Fact] public async Task Unknown_size_is_not_checked() { (await ReadStatusAsync(AbCipDataType.Int, bufferSize: -1)).ShouldBe(AbCipStatusMapper.Good); } /// A bit-in-DINT read decodes from the parent's 4-byte buffer and is not width-checked. [Fact] public async Task Bit_index_read_is_not_width_checked() { (await ReadStatusAsync(AbCipDataType.Bool, bufferSize: 4, path: "Flags.3")).ShouldBe(AbCipStatusMapper.Good); } }