222 lines
8.7 KiB
C#
222 lines
8.7 KiB
C#
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<RecordingBuilder> Children { get; } = new();
|
|
public List<RecordingVariable> Variables { get; } = new();
|
|
public List<RecordingProperty> 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();
|
|
}
|
|
}
|