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 L5kIngestTests { private const string DeviceHost = "ab://10.10.10.1/0,1"; [Fact] public void Atomic_controller_scope_tag_becomes_AbCipTagDefinition() { const string body = """ TAG Motor1_Speed : DINT (ExternalAccess := Read/Write) := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var ingest = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }; var result = ingest.Ingest(doc); result.Tags.Count.ShouldBe(1); var tag = result.Tags[0]; tag.Name.ShouldBe("Motor1_Speed"); tag.DeviceHostAddress.ShouldBe(DeviceHost); tag.TagPath.ShouldBe("Motor1_Speed"); tag.DataType.ShouldBe(AbCipDataType.DInt); tag.Writable.ShouldBeTrue(); tag.Members.ShouldBeNull(); } [Fact] public void Program_scope_tag_uses_Program_prefix_and_compound_name() { const string body = """ PROGRAM MainProgram TAG StepIndex : DINT := 0; END_TAG END_PROGRAM """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc); result.Tags.Count.ShouldBe(1); result.Tags[0].Name.ShouldBe("MainProgram.StepIndex"); result.Tags[0].TagPath.ShouldBe("Program:MainProgram.StepIndex"); } [Fact] public void Alias_tag_is_skipped() { const string body = """ TAG Real : DINT := 0; Aliased : DINT (AliasFor := "Real"); END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc); result.SkippedAliasCount.ShouldBe(1); result.Tags.Count.ShouldBe(1); result.Tags.ShouldAllBe(t => t.Name != "Aliased"); } [Fact] public void ExternalAccess_None_tag_is_skipped() { const string body = """ TAG Hidden : DINT (ExternalAccess := None) := 0; Visible : DINT := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc); result.SkippedNoAccessCount.ShouldBe(1); result.Tags.Single().Name.ShouldBe("Visible"); } [Fact] public void ExternalAccess_ReadOnly_tag_becomes_non_writable() { const string body = """ TAG Sensor : REAL (ExternalAccess := Read Only) := 0.0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc); result.Tags.Single().Writable.ShouldBeFalse(); } [Fact] public void UDT_typed_tag_picks_up_member_layout_from_DATATYPE_block() { const string body = """ DATATYPE TankUDT MEMBER Level : REAL := 0.0; MEMBER Active : BOOL := 0; END_DATATYPE TAG Tank1 : TankUDT := [0.0, 0]; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.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[0].Name.ShouldBe("Level"); tag.Members[0].DataType.ShouldBe(AbCipDataType.Real); tag.Members[1].Name.ShouldBe("Active"); tag.Members[1].DataType.ShouldBe(AbCipDataType.Bool); } [Fact] public void Unknown_datatype_falls_through_as_structure_with_no_members() { const string body = """ TAG Mystery : SomeUnknownType := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc); var tag = result.Tags.Single(); tag.DataType.ShouldBe(AbCipDataType.Structure); tag.Members.ShouldBeNull(); } [Fact] public void Ingest_throws_when_DefaultDeviceHostAddress_missing() { var doc = new L5kDocument(new[] { new L5kTag("X", "DINT", null, null, null, null) }, Array.Empty()); Should.Throw(() => new L5kIngest().Ingest(doc)); } [Fact] public void NamePrefix_is_applied_to_imported_tags() { const string body = """ TAG Speed : DINT := 0; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost, NamePrefix = "PLC1_", }.Ingest(doc); result.Tags.Single().Name.ShouldBe("PLC1_Speed"); result.Tags.Single().TagPath.ShouldBe("Speed"); // path on the PLC stays unchanged } }