using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa; [Trait("Category", "Unit")] public sealed class EquipmentNodeWalkerTests { [Fact] public void Walk_EmptyContent_EmitsNothing() { var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, new EquipmentNamespaceContent([], [], [], [])); rec.Children.ShouldBeEmpty(); } [Fact] public void Walk_EmitsArea_Line_Equipment_Folders_In_UnsOrder() { var content = new EquipmentNamespaceContent( Areas: [Area("area-1", "warsaw"), Area("area-2", "berlin")], Lines: [Line("line-1", "area-1", "oven-line"), Line("line-2", "area-2", "press-line")], Equipment: [Eq("eq-1", "line-1", "oven-3"), Eq("eq-2", "line-2", "press-7")], Tags: []); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); rec.Children.Select(c => c.BrowseName).ShouldBe(["berlin", "warsaw"]); // ordered by Name var warsaw = rec.Children.First(c => c.BrowseName == "warsaw"); warsaw.Children.Select(c => c.BrowseName).ShouldBe(["oven-line"]); warsaw.Children[0].Children.Select(c => c.BrowseName).ShouldBe(["oven-3"]); } [Fact] public void Walk_AddsFiveIdentifierProperties_OnEquipmentNode_Skipping_NullZTagSapid() { var uuid = Guid.NewGuid(); var eq = Eq("eq-1", "line-1", "oven-3"); eq.EquipmentUuid = uuid; eq.MachineCode = "MC-42"; eq.ZTag = null; eq.SAPID = null; var content = new EquipmentNamespaceContent( [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); var equipmentNode = rec.Children[0].Children[0].Children[0]; var props = equipmentNode.Properties.Select(p => p.BrowseName).ToList(); props.ShouldContain("EquipmentId"); props.ShouldContain("EquipmentUuid"); props.ShouldContain("MachineCode"); props.ShouldNotContain("ZTag"); props.ShouldNotContain("SAPID"); equipmentNode.Properties.First(p => p.BrowseName == "EquipmentUuid").Value.ShouldBe(uuid.ToString()); } [Fact] public void Walk_Adds_ZTag_And_SAPID_When_Present() { var eq = Eq("eq-1", "line-1", "oven-3"); eq.ZTag = "ZT-0042"; eq.SAPID = "10000042"; var content = new EquipmentNamespaceContent( [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); var equipmentNode = rec.Children[0].Children[0].Children[0]; equipmentNode.Properties.First(p => p.BrowseName == "ZTag").Value.ShouldBe("ZT-0042"); equipmentNode.Properties.First(p => p.BrowseName == "SAPID").Value.ShouldBe("10000042"); } [Fact] public void Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent() { var eq = Eq("eq-1", "line-1", "oven-3"); eq.Manufacturer = "Trumpf"; eq.Model = "TruLaser-3030"; var content = new EquipmentNamespaceContent( [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); var equipmentNode = rec.Children[0].Children[0].Children[0]; var identification = equipmentNode.Children.FirstOrDefault(c => c.BrowseName == "Identification"); identification.ShouldNotBeNull(); identification!.Properties.Select(p => p.BrowseName).ShouldContain("Manufacturer"); identification.Properties.Select(p => p.BrowseName).ShouldContain("Model"); } [Fact] public void Walk_Omits_Identification_Subfolder_When_AllFieldsNull() { var eq = Eq("eq-1", "line-1", "oven-3"); // no identification fields var content = new EquipmentNamespaceContent( [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); var equipmentNode = rec.Children[0].Children[0].Children[0]; equipmentNode.Children.ShouldNotContain(c => c.BrowseName == "Identification"); } [Fact] public void Walk_Emits_Variable_Per_BoundTag_Under_Equipment() { var eq = Eq("eq-1", "line-1", "oven-3"); var tag1 = NewTag("tag-1", "Temperature", "Int32", "plcaddr-01", equipmentId: "eq-1"); var tag2 = NewTag("tag-2", "Setpoint", "Float32", "plcaddr-02", equipmentId: "eq-1"); var unboundTag = NewTag("tag-3", "Orphan", "Int32", "plcaddr-03", equipmentId: null); // SystemPlatform-style, walker skips var content = new EquipmentNamespaceContent( [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], [tag1, tag2, unboundTag]); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); var equipmentNode = rec.Children[0].Children[0].Children[0]; equipmentNode.Variables.Count.ShouldBe(2); equipmentNode.Variables.Select(v => v.BrowseName).ShouldBe(["Setpoint", "Temperature"]); equipmentNode.Variables.First(v => v.BrowseName == "Temperature").AttributeInfo.FullName.ShouldBe("plcaddr-01"); equipmentNode.Variables.First(v => v.BrowseName == "Setpoint").AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Float32); } [Fact] public void Walk_FallsBack_To_String_For_Unparseable_DataType() { var eq = Eq("eq-1", "line-1", "oven-3"); var tag = NewTag("tag-1", "Mystery", "NotARealType", "plcaddr-42", equipmentId: "eq-1"); var content = new EquipmentNamespaceContent( [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], [tag]); var rec = new RecordingBuilder("root"); EquipmentNodeWalker.Walk(rec, content); var variable = rec.Children[0].Children[0].Children[0].Variables.Single(); variable.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.String); } // ----- builders for test seed rows ----- private static UnsArea Area(string id, string name) => new() { UnsAreaId = id, ClusterId = "c1", Name = name, GenerationId = 1, }; private static UnsLine Line(string id, string areaId, string name) => new() { UnsLineId = id, UnsAreaId = areaId, Name = name, GenerationId = 1, }; private static Equipment Eq(string equipmentId, string lineId, string name) => new() { EquipmentRowId = Guid.NewGuid(), GenerationId = 1, EquipmentId = equipmentId, EquipmentUuid = Guid.NewGuid(), DriverInstanceId = "drv", UnsLineId = lineId, Name = name, MachineCode = "MC-" + name, }; private static Tag NewTag(string tagId, string name, string dataType, string address, string? equipmentId) => new() { TagRowId = Guid.NewGuid(), GenerationId = 1, TagId = tagId, DriverInstanceId = "drv", EquipmentId = equipmentId, Name = name, DataType = dataType, AccessLevel = ZB.MOM.WW.OtOpcUa.Configuration.Enums.TagAccessLevel.ReadWrite, TagConfig = address, }; // ----- recording IAddressSpaceBuilder ----- private sealed class RecordingBuilder(string browseName) : IAddressSpaceBuilder { public string BrowseName { get; } = browseName; public List Children { get; } = new(); public List Variables { get; } = new(); public List Properties { get; } = new(); public IAddressSpaceBuilder Folder(string name, string _) { var child = new RecordingBuilder(name); Children.Add(child); return child; } public IVariableHandle Variable(string name, string _, DriverAttributeInfo attr) { var v = new RecordingVariable(name, attr); Variables.Add(v); return v; } public void AddProperty(string name, DriverDataType _, object? value) => Properties.Add(new RecordingProperty(name, value)); } private sealed record RecordingProperty(string BrowseName, object? Value); private sealed record RecordingVariable(string BrowseName, DriverAttributeInfo AttributeInfo) : IVariableHandle { public string FullReference => AttributeInfo.FullName; public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => throw new NotSupportedException(); } }