feat(v3-batch4-wp3): raw-only binding + UNS fan-out + write routing
Wave B of Batch 4 — the runtime binding seam for the dual namespace. Applier (both realms, explicit realm at every sink call site): - MaterialiseRawSubtree: Raw containers as folders + Raw tags as variables keyed by RawPath (native-alarm tag → single Part 9 condition at the RawPath), all in AddressSpaceRealm.Raw; historian tagname = override else RawPath. - MaterialiseUnsReferences: each UNS reference Variable under its equipment folder (Uns realm) + an Organizes UNS->Raw edge; inherits writable/array/ historian tagname from the backing raw tag (both NodeIds -> one tagname). - FeedHistorizedRefs / ProvisionHistorizedTags now source RAW tags (mux ref stays single, keyed by RawPath); ApplyPureRemove tears down raw tags + UNS refs in place (raw-container removal falls back to rebuild). DriverHostActor (dual-NodeId, single-source fan-out): - _nodeIdByDriverRef value gains a realm (NodeRealmRef); rebuilt from RawTags UNION UnsReferenceVariables so one (DriverInstanceId, RawPath) fans to the raw NodeId AND every referencing UNS NodeId with identical value/quality/timestamp. Write inverse map keyed by the bare id; the ns-qualified NodeId the write hook passes is normalised (BareNodeId) so a write to either NodeId resolves the same driver ref (-> RawPath write). - Native raw alarm condition routing is realm-tagged (Raw); AttributeValueUpdate + AlarmStateUpdate carry the realm through to the sink. Retire EquipmentNodeIds -> V3NodeIds.Uns (applier/VirtualTagHostActor) and RawPaths.Combine (DiscoveredNodeMapper, discovered nodes are Raw now). DeploymentArtifact.ParseComposition emits the Raw + UNS subtrees byte-parity with the composer (reconstruct entities -> AddressSpaceComposer.Compose). Sink surface: removed the transitional `= AddressSpaceRealm.Uns` defaults from IOpcUaAddressSpaceSink / ISurgicalAddressSpaceSink / SdkAddressSpaceSink / DeferredAddressSpaceSink / NullOpcUaAddressSpaceSink — every call site is now explicit (realm reordered before the trailing optionals on EnsureVariable + MaterialiseAlarmCondition). Node-manager convenience methods keep their defaults (they are not the interface impl; Sdk delegates explicitly). Tests: rewrote DriverHostActorLiveValueTests (fan-out drift, 1:N) + DriverHostActorWriteRoutingTests (dual-NodeId raw/uns write routing) to the v3 raw+uns model; new AddressSpaceApplierRawUnsTests; migrated the EquipmentTags provisioning/feed tests to RawTags; swept EquipmentNodeIds test callers. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -19,7 +19,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
{
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
// Must not throw.
|
||||
sink.WriteValue("ns=2;s=x", 42, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
sink.WriteValue("ns=2;s=x", 42, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -34,7 +34,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
{
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.UpdateTagAttributes("ns=2;s=x", writable: true, historianTagname: null,
|
||||
dataType: "Boolean", isArray: false, arrayLength: null).ShouldBeFalse();
|
||||
dataType: "Boolean", isArray: false, arrayLength: null, AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---------- after SetSink — operations are forwarded ----------
|
||||
@@ -46,7 +46,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.SetSink(inner);
|
||||
|
||||
sink.WriteValue("ns=2;s=x", 99, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
sink.WriteValue("ns=2;s=x", 99, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
inner.WriteValueCalled.ShouldBeTrue();
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
sink.SetSink(new SpySink());
|
||||
|
||||
sink.UpdateTagAttributes("ns=2;s=x", writable: true, historianTagname: null,
|
||||
dataType: "Int32", isArray: false, arrayLength: null).ShouldBeFalse();
|
||||
dataType: "Int32", isArray: false, arrayLength: null, AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -84,7 +84,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
sink.SetSink(surgical);
|
||||
|
||||
var result = sink.UpdateTagAttributes("ns=2;s=x", writable: true, historianTagname: null,
|
||||
dataType: "Float", isArray: false, arrayLength: null);
|
||||
dataType: "Float", isArray: false, arrayLength: null, AddressSpaceRealm.Uns);
|
||||
|
||||
result.ShouldBeTrue();
|
||||
surgical.UpdateCalled.ShouldBeTrue();
|
||||
@@ -97,7 +97,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.SetSink(new SpySink());
|
||||
|
||||
sink.UpdateFolderDisplayName("area-1", "Plant South").ShouldBeFalse();
|
||||
sink.UpdateFolderDisplayName("area-1", "Plant South", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -108,7 +108,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.SetSink(surgical);
|
||||
|
||||
var result = sink.UpdateFolderDisplayName("area-1", "Plant South");
|
||||
var result = sink.UpdateFolderDisplayName("area-1", "Plant South", AddressSpaceRealm.Uns);
|
||||
|
||||
result.ShouldBeTrue();
|
||||
surgical.FolderRenameCalled.ShouldBeTrue();
|
||||
@@ -122,9 +122,9 @@ public class DeferredAddressSpaceSinkTests
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.SetSink(new SpySink());
|
||||
|
||||
sink.RemoveVariableNode("v-1").ShouldBeFalse();
|
||||
sink.RemoveAlarmConditionNode("a-1").ShouldBeFalse();
|
||||
sink.RemoveEquipmentSubtree("eq-1").ShouldBeFalse();
|
||||
sink.RemoveVariableNode("v-1", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
sink.RemoveAlarmConditionNode("a-1", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
sink.RemoveEquipmentSubtree("eq-1", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -134,9 +134,9 @@ public class DeferredAddressSpaceSinkTests
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.SetSink(surgical);
|
||||
|
||||
sink.RemoveVariableNode("v-1").ShouldBeTrue();
|
||||
sink.RemoveAlarmConditionNode("a-1").ShouldBeTrue();
|
||||
sink.RemoveEquipmentSubtree("eq-1").ShouldBeTrue();
|
||||
sink.RemoveVariableNode("v-1", AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
sink.RemoveAlarmConditionNode("a-1", AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
sink.RemoveEquipmentSubtree("eq-1", AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
|
||||
surgical.RemoveVariableCalled.ShouldBeTrue();
|
||||
surgical.RemoveAlarmCalled.ShouldBeTrue();
|
||||
@@ -147,9 +147,9 @@ public class DeferredAddressSpaceSinkTests
|
||||
public void Before_SetSink_remove_members_return_false()
|
||||
{
|
||||
var sink = new DeferredAddressSpaceSink();
|
||||
sink.RemoveVariableNode("v-1").ShouldBeFalse();
|
||||
sink.RemoveAlarmConditionNode("a-1").ShouldBeFalse();
|
||||
sink.RemoveEquipmentSubtree("eq-1").ShouldBeFalse();
|
||||
sink.RemoveVariableNode("v-1", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
sink.RemoveAlarmConditionNode("a-1", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
sink.RemoveEquipmentSubtree("eq-1", AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---------- SetSink(null) reverts to null sink ----------
|
||||
@@ -163,7 +163,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
sink.SetSink(null); // revert
|
||||
|
||||
sink.UpdateTagAttributes("ns=2;s=x", writable: false, historianTagname: null,
|
||||
dataType: "Boolean", isArray: false, arrayLength: null).ShouldBeFalse();
|
||||
dataType: "Boolean", isArray: false, arrayLength: null, AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -174,7 +174,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
sink.SetSink(inner);
|
||||
sink.SetSink(null); // revert
|
||||
|
||||
sink.WriteValue("ns=2;s=x", 1, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
sink.WriteValue("ns=2;s=x", 1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
inner.WriteValueCalled.ShouldBeFalse("write should be no-op after reverting to null sink");
|
||||
}
|
||||
@@ -190,9 +190,9 @@ public class DeferredAddressSpaceSinkTests
|
||||
=> WriteValueCalled = true;
|
||||
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void RebuildAddressSpace() => RebuildCalled = true;
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
@@ -205,9 +205,9 @@ public class DeferredAddressSpaceSinkTests
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
||||
public void RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.OpcUa;
|
||||
|
||||
public class EquipmentNodeIdsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Variable_with_no_folder_is_equipment_slash_name()
|
||||
=> EquipmentNodeIds.Variable("eq-1", "", "speed").ShouldBe("eq-1/speed");
|
||||
|
||||
[Fact]
|
||||
public void Variable_with_null_folder_is_equipment_slash_name()
|
||||
=> EquipmentNodeIds.Variable("eq-1", null, "speed").ShouldBe("eq-1/speed");
|
||||
|
||||
[Fact]
|
||||
public void Variable_with_folder_is_equipment_slash_folder_slash_name()
|
||||
=> EquipmentNodeIds.Variable("eq-1", "registers", "speed").ShouldBe("eq-1/registers/speed");
|
||||
|
||||
[Fact]
|
||||
public void Variable_with_whitespace_folder_is_equipment_slash_name()
|
||||
=> EquipmentNodeIds.Variable("eq-1", " ", "speed").ShouldBe("eq-1/speed");
|
||||
|
||||
[Fact]
|
||||
public void SubFolder_is_equipment_slash_folder()
|
||||
=> EquipmentNodeIds.SubFolder("eq-1", "registers").ShouldBe("eq-1/registers");
|
||||
}
|
||||
Reference in New Issue
Block a user