fix(otopcua): guard root-level discovered var parent + tighten mapper
This commit is contained in:
@@ -5,6 +5,13 @@ using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
|
||||
/// <summary>The mapped result of grafting discovered nodes under an equipment node.</summary>
|
||||
/// <param name="Folders">
|
||||
/// Folders to ensure, in insertion order (parent-before-child within each node's prefix chain) — NOT
|
||||
/// globally depth-sorted. The applier sorts by depth before ensuring, so consumers must not assume a
|
||||
/// global parent-before-child ordering across the whole list.
|
||||
/// </param>
|
||||
/// <param name="Variables">Variables to ensure under the (post-collapse) folders.</param>
|
||||
/// <param name="RoutingByRef">Driver FullReference -> equipment NodeId, for live-value routing.</param>
|
||||
public sealed record DiscoveredInjectionPlan(
|
||||
IReadOnlyList<DiscoveredFolder> Folders,
|
||||
IReadOnlyList<DiscoveredVariable> Variables,
|
||||
@@ -29,7 +36,7 @@ public static class DiscoveredNodeMapper
|
||||
/// </param>
|
||||
/// <returns>The folders, variables, and routing map to apply against the OPC UA address space.</returns>
|
||||
public static DiscoveredInjectionPlan Map(
|
||||
string equipmentId, IReadOnlyList<DiscoveredNode> nodes, ISet<string> authoredRefs)
|
||||
string equipmentId, IReadOnlyList<DiscoveredNode> nodes, IReadOnlySet<string> authoredRefs)
|
||||
{
|
||||
var kept = nodes.Where(n => !authoredRefs.Contains(n.FullReference)).ToList();
|
||||
|
||||
@@ -65,7 +72,12 @@ public static class DiscoveredNodeMapper
|
||||
|
||||
var varFolderPath = string.Join('/', segs);
|
||||
var varNodeId = EquipmentNodeIds.Variable(equipmentId, varFolderPath, n.BrowseName);
|
||||
var varParent = EquipmentNodeIds.SubFolder(equipmentId, varFolderPath);
|
||||
// Mirror AddressSpaceApplier.MaterialiseEquipmentTags: a folder-less variable parents directly
|
||||
// at the equipment (SubFolder("", ...) would yield a trailing-slash "EQ-1/" that mismatches the
|
||||
// EquipmentNodeIds.Variable NodeId, which guards IsNullOrWhiteSpace).
|
||||
var varParent = string.IsNullOrEmpty(varFolderPath)
|
||||
? equipmentId
|
||||
: EquipmentNodeIds.SubFolder(equipmentId, varFolderPath);
|
||||
variables.Add(new DiscoveredVariable(
|
||||
varNodeId, varParent, n.DisplayName, ToBuiltinTypeString(n.DataType), n.Writable, n.IsArray, n.ArrayDim));
|
||||
routing[n.FullReference] = varNodeId;
|
||||
|
||||
@@ -63,6 +63,36 @@ public sealed class DiscoveredNodeMapperTests
|
||||
}, ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_input_yields_empty_plan()
|
||||
{
|
||||
var result = DiscoveredNodeMapper.Map("EQ-1", Array.Empty<DiscoveredNode>(), authoredRefs: new HashSet<string>());
|
||||
result.Folders.ShouldBeEmpty();
|
||||
result.Variables.ShouldBeEmpty();
|
||||
result.RoutingByRef.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Array_metadata_passes_through_unchanged()
|
||||
{
|
||||
var node = new DiscoveredNode(
|
||||
FolderPathSegments: ["FOCAS", "10.0.0.5:8193", "Axes"],
|
||||
BrowseName: "Positions",
|
||||
DisplayName: "Positions",
|
||||
FullReference: "10.0.0.5:8193/Axes/Positions",
|
||||
DataType: DriverDataType.Float64,
|
||||
IsArray: true,
|
||||
ArrayDim: 8u,
|
||||
Writable: false,
|
||||
IsHistorized: false);
|
||||
|
||||
var result = DiscoveredNodeMapper.Map("EQ-1", new[] { node }, authoredRefs: new HashSet<string>());
|
||||
|
||||
result.Variables.ShouldHaveSingleItem();
|
||||
result.Variables[0].IsArray.ShouldBeTrue();
|
||||
result.Variables[0].ArrayLength.ShouldBe(8u);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Mirror OtOpcUaNodeManager.ResolveBuiltInDataType's accepted string set: Float32 -> "Float",
|
||||
// Float64 -> "Double", Reference (Galaxy attr ref encoded as a string) -> "String". The pass-through
|
||||
|
||||
Reference in New Issue
Block a user