using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class AbCipTagPathTests { [Fact] public void Controller_scope_single_segment() { var p = AbCipTagPath.TryParse("Motor1_Speed"); p.ShouldNotBeNull(); p.ProgramScope.ShouldBeNull(); p.Segments.Count.ShouldBe(1); p.Segments[0].Name.ShouldBe("Motor1_Speed"); p.Segments[0].Subscripts.ShouldBeEmpty(); p.BitIndex.ShouldBeNull(); p.ToLibplctagName().ShouldBe("Motor1_Speed"); } [Fact] public void Program_scope_parses() { var p = AbCipTagPath.TryParse("Program:MainProgram.StepIndex"); p.ShouldNotBeNull(); p.ProgramScope.ShouldBe("MainProgram"); p.Segments.Single().Name.ShouldBe("StepIndex"); p.ToLibplctagName().ShouldBe("Program:MainProgram.StepIndex"); } [Fact] public void Structured_member_access_splits_segments() { var p = AbCipTagPath.TryParse("Motor1.Speed.Setpoint"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["Motor1", "Speed", "Setpoint"]); p.ToLibplctagName().ShouldBe("Motor1.Speed.Setpoint"); } [Fact] public void Single_dim_array_subscript() { var p = AbCipTagPath.TryParse("Data[7]"); p.ShouldNotBeNull(); p.Segments.Single().Name.ShouldBe("Data"); p.Segments.Single().Subscripts.ShouldBe([7]); p.ToLibplctagName().ShouldBe("Data[7]"); } [Fact] public void Multi_dim_array_subscript() { var p = AbCipTagPath.TryParse("Matrix[1,2,3]"); p.ShouldNotBeNull(); p.Segments.Single().Subscripts.ShouldBe([1, 2, 3]); p.ToLibplctagName().ShouldBe("Matrix[1,2,3]"); } [Fact] public void Bit_in_dint_captured_as_bit_index() { var p = AbCipTagPath.TryParse("Flags.3"); p.ShouldNotBeNull(); p.Segments.Single().Name.ShouldBe("Flags"); p.BitIndex.ShouldBe(3); p.ToLibplctagName().ShouldBe("Flags.3"); } [Fact] public void Bit_in_dint_after_member() { var p = AbCipTagPath.TryParse("Motor.Status.12"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["Motor", "Status"]); p.BitIndex.ShouldBe(12); p.ToLibplctagName().ShouldBe("Motor.Status.12"); } [Fact] public void Bit_index_32_rejected_out_of_range() { // 32 exceeds the DINT bit width — treated as a member name rather than bit selector, // which fails ident validation and returns null. AbCipTagPath.TryParse("Flags.32").ShouldBeNull(); } [Fact] public void Program_scope_with_members_and_subscript_and_bit() { var p = AbCipTagPath.TryParse("Program:MainProgram.Motors[0].Status.5"); p.ShouldNotBeNull(); p.ProgramScope.ShouldBe("MainProgram"); p.Segments.Select(s => s.Name).ShouldBe(["Motors", "Status"]); p.Segments[0].Subscripts.ShouldBe([0]); p.BitIndex.ShouldBe(5); p.ToLibplctagName().ShouldBe("Program:MainProgram.Motors[0].Status.5"); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("Program:")] // empty scope [InlineData("Program:MP")] // no body after scope [InlineData("1InvalidStart")] // ident starts with digit [InlineData("Bad Name")] // space in ident [InlineData("Motor[]")] // empty subscript [InlineData("Motor[-1]")] // negative subscript [InlineData("Motor[a]")] // non-numeric subscript [InlineData("Motor[")] // unbalanced bracket [InlineData("Motor.")] // trailing dot [InlineData(".Motor")] // leading dot public void Invalid_shapes_return_null(string? input) { AbCipTagPath.TryParse(input).ShouldBeNull(); } [Fact] public void Ident_with_underscore_accepted() { AbCipTagPath.TryParse("_private_tag")!.Segments.Single().Name.ShouldBe("_private_tag"); } [Fact] public void ToLibplctagName_recomposes_round_trip() { var cases = new[] { "Motor1_Speed", "Program:Main.Counter", "Array[5]", "Matrix[1,2]", "Obj.Member.Sub", "Flags.0", "Program:P.Obj[2].Flags.15", }; foreach (var c in cases) { var parsed = AbCipTagPath.TryParse(c); parsed.ShouldNotBeNull(c); parsed.ToLibplctagName().ShouldBe(c); } } }