using Shouldly; using TwinCAT.Ads; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; /// The pure sum-read decisions (Gitea #645) — the part of the wire path a fake client cannot reach. [Trait("Category", "Unit")] public sealed class TwinCATSumReadPlannerTests { private static TwinCATPlcSymbolShape Plc(string name, int size, bool isFloat = false) => new(TwinCATPlcKind.Scalar, size, isFloat, name); /// Verifies the strict width + kind rule: only an exact match rides a sum command, the lenient /// CheckRead pairs (value-converted by Beckhoff on a single read) are read singly instead. [Theory] [InlineData(TwinCATDataType.DInt, "DINT", 4, false, true)] [InlineData(TwinCATDataType.DInt, "UDINT", 4, false, true)] // same width + kind: tolerated [InlineData(TwinCATDataType.Real, "REAL", 4, true, true)] [InlineData(TwinCATDataType.Bool, "BOOL", 1, false, true)] [InlineData(TwinCATDataType.DInt, "INT", 2, false, false)] // would over-read into the neighbour [InlineData(TwinCATDataType.Real, "LREAL", 8, true, false)] // half a double [InlineData(TwinCATDataType.Real, "DINT", 4, false, false)] // bit pattern reinterpreted [InlineData(TwinCATDataType.DInt, "REAL", 4, true, false)] [InlineData(TwinCATDataType.Int, "DINT", 4, false, false)] // truncation public void Scalar_eligibility_needs_exact_width_and_kind( TwinCATDataType declared, string plcType, int plcSize, bool plcFloat, bool eligible) { var shape = Plc(plcType, plcSize, plcFloat); TwinCATSumReadPlanner.ScalarEligible(declared, shape).ShouldBe(eligible); // The mismatching pairs are exactly the ones the lenient single-read check lets through. if (!eligible) TwinCATTypeGuard.CheckRead(declared, shape).ShouldBeNull(); } /// Verifies a string tag over a numeric symbol is not eligible. [Fact] public void A_string_tag_over_a_numeric_symbol_is_not_eligible() => TwinCATSumReadPlanner.ScalarEligible(TwinCATDataType.String, Plc("DINT", 4)).ShouldBeFalse(); /// Verifies only service / index-group errors latch; request-size errors split without latching. [Theory] [InlineData(1793u, true, false)] [InlineData(1794u, true, false)] [InlineData(1792u, false, true)] [InlineData(1797u, false, true)] [InlineData(1798u, false, true)] [InlineData(1795u, false, true)] // invalid offset: this request rejected, read singly [InlineData(1796u, false, true)] [InlineData(1808u, false, true)] [InlineData(6u, false, false)] // comms: every item goes Bad [InlineData(7u, false, false)] [InlineData(1861u, false, false)] public void Overall_ads_codes_classify(uint code, bool refusal, bool rejected) { TwinCATSumReadPlanner.IsRefusal(code).ShouldBe(refusal); TwinCATSumReadPlanner.IsRequestRejected(code).ShouldBe(rejected); } /// Verifies the byte budget splits a poll and gives an oversized item a group of its own. [Fact] public void Partition_respects_the_byte_budget() { // cost = length + 4: 104, 104, 104 | 1004 (own group) | 14 var groups = TwinCATSumReadPlanner.Partition([100, 100, 100, 1000, 10], 330); groups.Select(g => g.ToArray()).ToArray().ShouldBe([[0, 1, 2], [3], [4]]); } /// Verifies an empty poll partitions to nothing. [Fact] public void Partition_of_nothing_is_empty() => TwinCATSumReadPlanner.Partition([], 1000).ShouldBeEmpty(); /// Verifies a Good block decodes to a scalar / an array. [Fact] public void Decode_returns_scalar_and_array_values() { byte[] four = [0x2A, 0, 0, 0]; var scalar = TwinCATSumReadPlanner.DecodeItem(TwinCATDataType.DInt, AdsErrorCode.NoError, four, new TwinCATArrayLayout(1, 4), false, false, TwinCATStatusMapper.MapAdsError); scalar.ShouldBe((42, TwinCATStatusMapper.Good)); byte[] arr = [1, 0, 2, 0]; var array = TwinCATSumReadPlanner.DecodeItem(TwinCATDataType.Int, AdsErrorCode.NoError, arr, new TwinCATArrayLayout(2, 2), true, false, TwinCATStatusMapper.MapAdsError); array.status.ShouldBe(TwinCATStatusMapper.Good); array.value.ShouldBe(new short[] { 1, 2 }); } /// Verifies a per-item ADS error maps to that item. [Fact] public void Decode_maps_a_sub_error() => TwinCATSumReadPlanner.DecodeItem(TwinCATDataType.DInt, (AdsErrorCode)1808, null, new TwinCATArrayLayout(1, 4), false, false, TwinCATStatusMapper.MapAdsError) .ShouldBe((null, TwinCATStatusMapper.BadNodeIdUnknown)); /// Verifies a short or missing block is a size error for that item, never an uncaught throw. [Fact] public void Decode_of_a_short_or_missing_block_is_a_per_item_size_error() { var expected = (default(object), TwinCATStatusMapper.MapAdsError(1797)); TwinCATSumReadPlanner.DecodeItem(TwinCATDataType.DInt, AdsErrorCode.NoError, new byte[] { 1, 2 }, new TwinCATArrayLayout(1, 4), false, false, TwinCATStatusMapper.MapAdsError).ShouldBe(expected); TwinCATSumReadPlanner.DecodeItem(TwinCATDataType.DInt, AdsErrorCode.NoError, null, new TwinCATArrayLayout(1, 4), false, false, TwinCATStatusMapper.MapAdsError).ShouldBe(expected); } }