using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests; /// /// Config-time guard on the two span-read knobs. There is no IValidateOptions machinery /// for S7 — is the single choke point every /// deployed config passes through — so a nonsensical value used to be accepted at deploy time and /// only misbehave later, in the read loop, on a driver that had already reported healthy. /// /// /// The legal-value cases matter as much as the rejections: -1 on the gap is the documented /// escape hatch that disables grouping entirely (S7Driver enables spans on >= 0), /// and 0 means "tolerate no gap at all", i.e. merge only strictly adjacent tags. A guard /// written as < 0 would break both. /// public sealed class S7SpanOptionValidationTests { /// A driver-config blob carrying the given span knob, host included (it is required). private static string Config(string knob, int value) => "{\"host\":\"10.0.0.1\",\"" + knob + "\":" + value.ToString(System.Globalization.CultureInfo.InvariantCulture) + "}"; /// A span cap below one can never admit a single planned block read, so it is rejected. [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(-220)] public void A_span_cap_below_one_is_rejected(int maxSpanBytes) { var ex = Should.Throw(() => S7DriverFactoryExtensions.ParseOptions( "s7-cfg", Config("spanReadMaxSpanBytes", maxSpanBytes))); ex.Message.ShouldContain("SpanReadMaxSpanBytes"); ex.Message.ShouldContain("s7-cfg"); } /// A gap budget below the grouping-disabled sentinel is rejected. [Theory] [InlineData(-2)] [InlineData(-16)] public void A_gap_budget_below_minus_one_is_rejected(int maxGapBytes) { var ex = Should.Throw(() => S7DriverFactoryExtensions.ParseOptions( "s7-cfg", Config("spanReadMaxGapBytes", maxGapBytes))); ex.Message.ShouldContain("SpanReadMaxGapBytes"); ex.Message.ShouldContain("s7-cfg"); } /// /// The two legal edge values survive: -1 (grouping disabled) and 0 (no gap /// tolerated). Both are documented behaviour, not degenerate input. /// [Theory] [InlineData(-1)] [InlineData(0)] public void The_documented_gap_edge_values_are_accepted(int maxGapBytes) { var parsed = S7DriverFactoryExtensions.ParseOptions("s7-cfg", Config("spanReadMaxGapBytes", maxGapBytes)); parsed.SpanReadMaxGapBytes.ShouldBe(maxGapBytes); } /// A one-byte span cap is the smallest legal cap and must not trip the guard. [Fact] public void The_smallest_legal_span_cap_is_accepted() { var parsed = S7DriverFactoryExtensions.ParseOptions("s7-cfg", Config("spanReadMaxSpanBytes", 1)); parsed.SpanReadMaxSpanBytes.ShouldBe(1); } /// /// An over-PDU span cap is legal — S7.Net transparently chunks an oversized ReadBytes /// into several wire requests — so the guard must not invent an upper bound it cannot know /// (the negotiated PDU size is opaque inside S7.Net). /// [Fact] public void An_over_pdu_span_cap_is_accepted_because_S7Net_chunks_it() { var parsed = S7DriverFactoryExtensions.ParseOptions("s7-cfg", Config("spanReadMaxSpanBytes", 4096)); parsed.SpanReadMaxSpanBytes.ShouldBe(4096); } /// Omitting both knobs keeps the shipped defaults, which must themselves pass the guard. [Fact] public void The_shipped_defaults_pass_the_guard() { var parsed = S7DriverFactoryExtensions.ParseOptions("s7-cfg", "{\"host\":\"10.0.0.1\"}"); parsed.SpanReadMaxGapBytes.ShouldBe(S7DriverOptions.DefaultSpanReadMaxGapBytes); parsed.SpanReadMaxSpanBytes.ShouldBe(S7DriverOptions.DefaultSpanReadMaxSpanBytes); } }