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 AOI_definition_block_collects_parameters_with_Usage() { // PR abcip-2.6 — ADD_ON_INSTRUCTION_DEFINITION blocks with PARAMETER entries carrying // Usage := Input / Output / InOut. The parser surfaces them as L5kDataType members so // AOI-typed tags pick up a layout the same way UDT-typed tags do. const string body = """ ADD_ON_INSTRUCTION_DEFINITION MyValveAoi (Revision := "1.0") PARAMETERS PARAMETER Cmd : BOOL (Usage := Input) := 0; PARAMETER Status : DINT (Usage := Output, ExternalAccess := Read Only) := 0; PARAMETER Buffer : DINT (Usage := InOut) := 0; PARAMETER Internal : DINT := 0; END_PARAMETERS LOCAL_TAGS Working : DINT := 0; END_LOCAL_TAGS END_ADD_ON_INSTRUCTION_DEFINITION """; var doc = L5kParser.Parse(new StringL5kSource(body)); var aoi = doc.DataTypes.Single(d => d.Name == "MyValveAoi"); aoi.Members.Count.ShouldBe(4); aoi.Members.Single(m => m.Name == "Cmd").Usage.ShouldBe("Input"); aoi.Members.Single(m => m.Name == "Status").Usage.ShouldBe("Output"); aoi.Members.Single(m => m.Name == "Buffer").Usage.ShouldBe("InOut"); aoi.Members.Single(m => m.Name == "Internal").Usage.ShouldBeNull(); } [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"); } }