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 L5xParserTests { [Fact] public void Controller_scope_Tag_elements_parse_with_metadata() { const string body = """ """; var doc = L5xParser.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].ExternalAccess.ShouldBe("Read/Write"); doc.Tags[0].Description.ShouldBe("Motor 1 set point"); doc.Tags[0].ProgramScope.ShouldBeNull(); doc.Tags[0].AliasFor.ShouldBeNull(); doc.Tags[1].Name.ShouldBe("Tank_Level"); doc.Tags[1].ExternalAccess.ShouldBe("Read Only"); } [Fact] public void Program_scope_Tag_elements_carry_program_name() { const string body = """ """; var doc = L5xParser.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_carries_AliasFor_and_is_skipped_on_ingest() { const string body = """ """; var doc = L5xParser.Parse(new StringL5kSource(body)); var alias = doc.Tags.Single(t => t.Name == "Aliased"); alias.AliasFor.ShouldBe("Real"); var ingestResult = new L5kIngest { DefaultDeviceHostAddress = "ab://10.0.0.1/0,1" }.Ingest(doc); ingestResult.SkippedAliasCount.ShouldBe(1); ingestResult.Tags.ShouldAllBe(t => t.Name != "Aliased"); } [Fact] public void DataType_block_collects_member_elements_and_skips_hidden_zzzz_host() { const string body = """ """; var doc = L5xParser.Parse(new StringL5kSource(body)); doc.DataTypes.Count.ShouldBe(1); var udt = doc.DataTypes[0]; udt.Name.ShouldBe("TankUDT"); udt.Members.Count.ShouldBe(2); udt.Members.ShouldContain(m => m.Name == "Level" && m.DataType == "REAL"); udt.Members.ShouldContain(m => m.Name == "Active"); } [Fact] public void UDT_typed_tag_picks_up_member_layout_through_ingest() { const string body = """ """; var doc = L5xParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = "ab://10.0.0.1/0,1" }.Ingest(doc); var tag = result.Tags.Single(); tag.Name.ShouldBe("Tank1"); tag.DataType.ShouldBe(AbCipDataType.Structure); tag.Members.ShouldNotBeNull(); tag.Members!.Count.ShouldBe(2); tag.Members.Select(m => m.Name).ShouldBe(["Level", "Pressure"]); } [Fact] public void AOI_definition_surfaces_as_datatype_with_visible_parameters() { // EnableIn / EnableOut on real exports carry Hidden="true" — the parser must skip those // so AOI-typed tags don't end up with phantom EnableIn/EnableOut members. const string body = """ """; var doc = L5xParser.Parse(new StringL5kSource(body)); // AOI definition should appear as a "DataType" entry alongside any UDTs. var aoi = doc.DataTypes.Single(d => d.Name == "MyValveAoi"); aoi.Members.Count.ShouldBe(2); aoi.Members.Select(m => m.Name).ShouldBe(["Cmd", "Status"]); var result = new L5kIngest { DefaultDeviceHostAddress = "ab://10.0.0.1/0,1" }.Ingest(doc); var tag = result.Tags.Single(); tag.Name.ShouldBe("Valve_001"); tag.DataType.ShouldBe(AbCipDataType.Structure); tag.Members.ShouldNotBeNull(); tag.Members!.Select(m => m.Name).ShouldBe(["Cmd", "Status"]); } [Fact] public void Empty_or_minimal_document_returns_empty_bundle_without_throwing() { const string body = """ """; var doc = L5xParser.Parse(new StringL5kSource(body)); doc.Tags.Count.ShouldBe(0); doc.DataTypes.Count.ShouldBe(0); } [Fact] public void Missing_external_access_defaults_to_writable_through_ingest() { // L5X: ExternalAccess attribute absent → ingest treats as default (writable, not skipped). const string body = """ """; var doc = L5xParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = "ab://10.0.0.1/0,1" }.Ingest(doc); result.Tags.Single().Writable.ShouldBeTrue(); result.SkippedNoAccessCount.ShouldBe(0); } }