using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// Version-matrix coverage for . Encodes the /// documented Fanuc FOCAS Developer Kit support boundaries per CNC series so a /// config-time change that widens or narrows a range without updating /// docs/v2/focas-version-matrix.md fails a test. Every assertion cites the /// specific matrix row it reflects. /// [Trait("Category", "Unit")] public sealed class FocasCapabilityMatrixTests { // ---- Macro ranges ---- [Theory] [InlineData(FocasCncSeries.Sixteen_i, 999, true)] [InlineData(FocasCncSeries.Sixteen_i, 1000, false)] // above legacy ceiling [InlineData(FocasCncSeries.Zero_i_D, 999, true)] [InlineData(FocasCncSeries.Zero_i_D, 9999, false)] // 0i-D is still legacy-ceiling [InlineData(FocasCncSeries.Zero_i_F, 9999, true)] // widened on 0i-F [InlineData(FocasCncSeries.Zero_i_F, 10000, false)] [InlineData(FocasCncSeries.Thirty_i, 99999, true)] // highest-end [InlineData(FocasCncSeries.Thirty_i, 100000, false)] [InlineData(FocasCncSeries.PowerMotion_i, 999, true)] [InlineData(FocasCncSeries.PowerMotion_i, 1000, false)] // atypical coverage public void Macro_range_matches_series(FocasCncSeries series, int number, bool accepted) { var address = new FocasAddress(FocasAreaKind.Macro, null, number, null); var result = FocasCapabilityMatrix.Validate(series, address); (result is null).ShouldBe(accepted, $"Macro #{number} on {series}: expected {(accepted ? "accept" : "reject")}, got {(result ?? "accept")}"); } // ---- Parameter ranges ---- [Theory] [InlineData(FocasCncSeries.Sixteen_i, 9999, true)] [InlineData(FocasCncSeries.Sixteen_i, 10000, false)] // 16i capped at 9999 [InlineData(FocasCncSeries.Zero_i_F, 14999, true)] [InlineData(FocasCncSeries.Zero_i_F, 15000, false)] [InlineData(FocasCncSeries.Thirty_i, 29999, true)] [InlineData(FocasCncSeries.Thirty_i, 30000, false)] public void Parameter_range_matches_series(FocasCncSeries series, int number, bool accepted) { var address = new FocasAddress(FocasAreaKind.Parameter, null, number, null); var result = FocasCapabilityMatrix.Validate(series, address); (result is null).ShouldBe(accepted); } // ---- PMC letters ---- [Theory] [InlineData(FocasCncSeries.Sixteen_i, "X", true)] [InlineData(FocasCncSeries.Sixteen_i, "Y", true)] [InlineData(FocasCncSeries.Sixteen_i, "R", true)] [InlineData(FocasCncSeries.Sixteen_i, "F", false)] // 16i has no F/G signal groups [InlineData(FocasCncSeries.Sixteen_i, "G", false)] [InlineData(FocasCncSeries.Sixteen_i, "K", false)] [InlineData(FocasCncSeries.Zero_i_D, "E", true)] // widened since 0i-D [InlineData(FocasCncSeries.Zero_i_D, "F", false)] // still no F on 0i-D [InlineData(FocasCncSeries.Zero_i_F, "F", true)] // F/G added on 0i-F [InlineData(FocasCncSeries.Zero_i_F, "K", false)] // K/T still 30i-only [InlineData(FocasCncSeries.Thirty_i, "K", true)] [InlineData(FocasCncSeries.Thirty_i, "T", true)] [InlineData(FocasCncSeries.Thirty_i, "Q", false)] // unsupported even on 30i public void Pmc_letter_matches_series(FocasCncSeries series, string letter, bool accepted) { var address = new FocasAddress(FocasAreaKind.Pmc, letter, 0, null); var result = FocasCapabilityMatrix.Validate(series, address); (result is null).ShouldBe(accepted, $"PMC letter '{letter}' on {series}: expected {(accepted ? "accept" : "reject")}, got {(result ?? "accept")}"); } // ---- PMC number ceiling ---- [Theory] [InlineData(FocasCncSeries.Sixteen_i, "R", 999, true)] [InlineData(FocasCncSeries.Sixteen_i, "R", 1000, false)] [InlineData(FocasCncSeries.Zero_i_D, "R", 1999, true)] [InlineData(FocasCncSeries.Zero_i_D, "R", 2000, false)] [InlineData(FocasCncSeries.Zero_i_F, "R", 9999, true)] [InlineData(FocasCncSeries.Zero_i_F, "R", 10000, false)] [InlineData(FocasCncSeries.Thirty_i, "R", 59999, true)] [InlineData(FocasCncSeries.Thirty_i, "R", 60000, false)] public void Pmc_number_ceiling_matches_series(FocasCncSeries series, string letter, int number, bool accepted) { var address = new FocasAddress(FocasAreaKind.Pmc, letter, number, null); var result = FocasCapabilityMatrix.Validate(series, address); (result is null).ShouldBe(accepted); } // ---- Unknown series is permissive ---- [Theory] [InlineData("Z", 999_999)] // absurd PMC address [InlineData("Q", 0)] // non-existent letter public void Unknown_series_accepts_any_PMC(string letter, int number) { var address = new FocasAddress(FocasAreaKind.Pmc, letter, number, null); FocasCapabilityMatrix.Validate(FocasCncSeries.Unknown, address).ShouldBeNull(); } [Fact] public void Unknown_series_accepts_any_macro_number() { var address = new FocasAddress(FocasAreaKind.Macro, null, 999_999, null); FocasCapabilityMatrix.Validate(FocasCncSeries.Unknown, address).ShouldBeNull(); } [Fact] public void Unknown_series_accepts_any_parameter_number() { var address = new FocasAddress(FocasAreaKind.Parameter, null, 999_999, null); FocasCapabilityMatrix.Validate(FocasCncSeries.Unknown, address).ShouldBeNull(); } // ---- Reason messages include enough context to diagnose ---- [Fact] public void Rejection_message_names_series_and_limit() { var address = new FocasAddress(FocasAreaKind.Macro, null, 100_000, null); var reason = FocasCapabilityMatrix.Validate(FocasCncSeries.Zero_i_F, address); reason.ShouldNotBeNull(); reason.ShouldContain("100000"); reason.ShouldContain("Zero_i_F"); reason.ShouldContain("9999"); } [Fact] public void Pmc_rejection_lists_accepted_letters() { var address = new FocasAddress(FocasAreaKind.Pmc, "Q", 0, null); var reason = FocasCapabilityMatrix.Validate(FocasCncSeries.Thirty_i, address); reason.ShouldNotBeNull(); reason.ShouldContain("'Q'"); reason.ShouldContain("X"); // some accepted letter should appear reason.ShouldContain("Y"); } // ---- PMC address letter is case-insensitive ---- [Theory] [InlineData("x")] [InlineData("X")] [InlineData("f")] public void Pmc_letter_match_is_case_insensitive_on_30i(string letter) { var address = new FocasAddress(FocasAreaKind.Pmc, letter, 0, null); FocasCapabilityMatrix.Validate(FocasCncSeries.Thirty_i, address).ShouldBeNull(); } }