using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip.Import; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class L5kParserTests { [Fact] public void Controller_scope_TAG_block_parses_name_datatype_externalaccess() { const string body = """ TAG Motor1_Speed : DINT (Description := "Motor 1 set point", ExternalAccess := Read/Write) := 0; Tank_Level : REAL (ExternalAccess := Read Only) := 0.0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); doc.Tags.Count.ShouldBe(2); doc.Tags[0].Name.ShouldBe("Motor1_Speed"); doc.Tags[0].DataType.ShouldBe("DINT"); doc.Tags[0].ProgramScope.ShouldBeNull(); doc.Tags[0].ExternalAccess.ShouldBe("Read/Write"); doc.Tags[0].Description.ShouldBe("Motor 1 set point"); doc.Tags[0].AliasFor.ShouldBeNull(); doc.Tags[1].Name.ShouldBe("Tank_Level"); doc.Tags[1].DataType.ShouldBe("REAL"); doc.Tags[1].ExternalAccess.ShouldBe("Read Only"); } [Fact] public void Program_scope_TAG_block_carries_program_name() { const string body = """ PROGRAM MainProgram (Class := Standard) TAG StepIndex : DINT := 0; Running : BOOL := 0; END_TAG END_PROGRAM """; var doc = L5kParser.Parse(new StringL5kSource(body)); doc.Tags.Count.ShouldBe(2); doc.Tags.ShouldAllBe(t => t.ProgramScope == "MainProgram"); doc.Tags.Select(t => t.Name).ShouldBe(["StepIndex", "Running"]); } [Fact] public void Alias_tag_is_flagged() { const string body = """ TAG Motor1 : DINT := 0; Motor1_Alias : DINT (AliasFor := "Motor1", ExternalAccess := Read/Write); END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var alias = doc.Tags.Single(t => t.Name == "Motor1_Alias"); alias.AliasFor.ShouldBe("Motor1"); } [Fact] public void DATATYPE_block_collects_member_lines() { const string body = """ DATATYPE TankUDT (FamilyType := NoFamily) MEMBER Level : REAL (ExternalAccess := Read/Write) := 0.0; MEMBER Pressure : REAL := 0.0; MEMBER Active : BOOL := 0; END_DATATYPE """; var doc = L5kParser.Parse(new StringL5kSource(body)); doc.DataTypes.Count.ShouldBe(1); var udt = doc.DataTypes[0]; udt.Name.ShouldBe("TankUDT"); udt.Members.Count.ShouldBe(3); udt.Members[0].Name.ShouldBe("Level"); udt.Members[0].DataType.ShouldBe("REAL"); udt.Members[0].ExternalAccess.ShouldBe("Read/Write"); udt.Members[1].Name.ShouldBe("Pressure"); udt.Members[2].Name.ShouldBe("Active"); udt.Members[2].DataType.ShouldBe("BOOL"); } [Fact] public void DATATYPE_member_with_array_dim_keeps_type_clean() { const string body = """ DATATYPE BatchUDT MEMBER Recipe : DINT[16] := 0; MEMBER Name : STRING := ""; END_DATATYPE """; var doc = L5kParser.Parse(new StringL5kSource(body)); var udt = doc.DataTypes[0]; var recipe = udt.Members.First(m => m.Name == "Recipe"); recipe.DataType.ShouldBe("DINT"); recipe.ArrayDim.ShouldBe(16); var nameMember = udt.Members.First(m => m.Name == "Name"); nameMember.DataType.ShouldBe("STRING"); nameMember.ArrayDim.ShouldBeNull(); } [Fact] public void Block_comments_are_stripped_before_parsing() { const string body = """ (* This is a long multi-line comment with TAG and END_TAG inside, parser must skip *) TAG Real_Tag : DINT := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); doc.Tags.Count.ShouldBe(1); doc.Tags[0].Name.ShouldBe("Real_Tag"); } [Fact] public void Unknown_sections_are_skipped_silently() { const string body = """ CONFIG SomeConfig (Class := Standard) ConfigData := 0; END_CONFIG MOTION_GROUP Motion1 Member := whatever; END_MOTION_GROUP TAG Real_Tag : DINT := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); doc.Tags.Count.ShouldBe(1); doc.Tags[0].Name.ShouldBe("Real_Tag"); } [Fact] public void Multi_line_TAG_entry_is_concatenated() { const string body = """ TAG Motor1 : DINT (Description := "Long description spanning", ExternalAccess := Read/Write) := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); doc.Tags.Count.ShouldBe(1); doc.Tags[0].Description.ShouldBe("Long description spanning"); doc.Tags[0].ExternalAccess.ShouldBe("Read/Write"); } }