using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; [Trait("Category", "Unit")] public sealed class TwinCATSymbolPathTests { [Fact] public void Single_segment_global_variable_parses() { var p = TwinCATSymbolPath.TryParse("Counter"); p.ShouldNotBeNull(); p.Segments.Single().Name.ShouldBe("Counter"); p.ToAdsSymbolName().ShouldBe("Counter"); } [Fact] public void POU_dot_variable_parses() { var p = TwinCATSymbolPath.TryParse("MAIN.bStart"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["MAIN", "bStart"]); p.ToAdsSymbolName().ShouldBe("MAIN.bStart"); } [Fact] public void GVL_reference_parses() { var p = TwinCATSymbolPath.TryParse("GVL.Counter"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["GVL", "Counter"]); p.ToAdsSymbolName().ShouldBe("GVL.Counter"); } [Fact] public void Structured_member_access_splits() { var p = TwinCATSymbolPath.TryParse("Motor1.Status.Running"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["Motor1", "Status", "Running"]); } [Fact] public void Array_subscript_parses() { var p = TwinCATSymbolPath.TryParse("Data[5]"); p.ShouldNotBeNull(); p.Segments.Single().Subscripts.ShouldBe([5]); p.ToAdsSymbolName().ShouldBe("Data[5]"); } [Fact] public void Multi_dim_array_subscript_parses() { var p = TwinCATSymbolPath.TryParse("Matrix[1,2]"); p.ShouldNotBeNull(); p.Segments.Single().Subscripts.ShouldBe([1, 2]); } [Fact] public void Bit_access_captured_as_bit_index() { var p = TwinCATSymbolPath.TryParse("Flags.3"); p.ShouldNotBeNull(); p.Segments.Single().Name.ShouldBe("Flags"); p.BitIndex.ShouldBe(3); p.ToAdsSymbolName().ShouldBe("Flags.3"); } [Fact] public void Bit_access_after_member_path() { var p = TwinCATSymbolPath.TryParse("GVL.Status.7"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["GVL", "Status"]); p.BitIndex.ShouldBe(7); } [Fact] public void Combined_scope_member_subscript_bit() { var p = TwinCATSymbolPath.TryParse("MAIN.Motors[0].Status.5"); p.ShouldNotBeNull(); p.Segments.Select(s => s.Name).ShouldBe(["MAIN", "Motors", "Status"]); p.Segments[1].Subscripts.ShouldBe([0]); p.BitIndex.ShouldBe(5); p.ToAdsSymbolName().ShouldBe("MAIN.Motors[0].Status.5"); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData(".Motor")] // leading dot [InlineData("Motor.")] // trailing dot [InlineData("Motor.[0]")] // empty segment [InlineData("1bad")] // 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("Flags.32")] // bit out of range (treated as ident → invalid shape) public void Invalid_shapes_return_null(string? input) { TwinCATSymbolPath.TryParse(input).ShouldBeNull(); } [Fact] public void Underscore_prefix_idents_accepted() { TwinCATSymbolPath.TryParse("_internal_var")!.Segments.Single().Name.ShouldBe("_internal_var"); } [Fact] public void ToAdsSymbolName_roundtrips() { var cases = new[] { "Counter", "MAIN.bStart", "GVL.Counter", "Motor1.Status.Running", "Data[5]", "Matrix[1,2]", "Flags.3", "MAIN.Motors[0].Status.5", }; foreach (var c in cases) { var parsed = TwinCATSymbolPath.TryParse(c); parsed.ShouldNotBeNull(c); parsed.ToAdsSymbolName().ShouldBe(c); } } }