fix(v3-batch4-wp3): realm-qualified write routing + dormant discovery guard + self-correction/byte-parity tests (Wave B review H1/M1/M2/L1/L3)
H1 (HIGH): write-routing key now (AddressSpaceRealm, bareId), not bare-only. A raw s=<RawPath> and a UNS s=<Area/Line/Equip/Eff> can collide as bare strings; the bare-only key let a colliding raw+UNS pair route to the WRONG driver ref (last-writer-wins). The realm the node manager resolves (RealmOf) is now threaded through IOpcUaNodeWriteGateway.WriteAsync -> RouteNodeWrite -> _driverRefByNodeId keyed by (realm, bareId). New regression test: Colliding_raw_and_uns_bare_ids_route_to_their_own_driver_by_realm. M1 (MEDIUM): discovered-node injection made coherently DORMANT. HandleDiscoveredNodes hard-short-circuits (single enforcement point; _discoveredByDriver never populates so the re-inject tail is inert too), with a clear log pointing at the /raw browse-commit flow. New pin: Discovered_nodes_are_ignored_dormant_in_v3; the 16+2 v2 injection scenarios re-pointed to an accurate skip reason (DiscoveryInjectionDormantV3). M2 (MEDIUM): realm-qualified dual-node self-correction tests — Failed_uns_write_reverts_uns_node_and_leaves_raw_node_untouched + Raw_realm_revert_reverts_raw_node_only (the second fails if the realm is dropped). L1: removed the = AddressSpaceRealm.Uns defaults from the consequential node-manager mutation methods (WriteValue/WriteAlarmCondition/MaterialiseAlarmCondition/ EnsureFolder/EnsureVariable/UpdateFolderDisplayName/UpdateTagAttributes/ RaiseNodesAddedModelChange/Remove*/RevertOptimisticWriteIfNeeded) + the AttributeValueUpdate/AlarmStateUpdate records, so the compiler forces explicit realm; read-only accessors + internal builders retain their defaults. L3: fixed the stale VirtualTagHostActor class comment (V3NodeIds.Uns, not the retired EquipmentNodeIds.Variable). Also: DeploymentArtifactRawUnsParityTests — Raw/UNS node-set byte-parity round-trip between AddressSpaceComposer.Compose and DeploymentArtifact.ParseComposition. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -34,8 +34,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-1");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnAcknowledge.ShouldNotBeNull();
|
||||
@@ -66,8 +66,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2");
|
||||
nm.MaterialiseAlarmCondition("alm-2", "eq-2", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-2", "eq-2", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-2");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -91,8 +91,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-3", parentNodeId: null, displayName: "Equipment 3");
|
||||
nm.MaterialiseAlarmCondition("alm-3", "eq-3", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-3", parentNodeId: null, displayName: "Equipment 3", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-3", "eq-3", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-3");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -115,8 +115,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-c", parentNodeId: null, displayName: "Equipment C");
|
||||
nm.MaterialiseAlarmCondition("alm-c", "eq-c", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-c", parentNodeId: null, displayName: "Equipment C", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-c", "eq-c", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-c");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnConfirm.ShouldNotBeNull();
|
||||
@@ -142,8 +142,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-ac", parentNodeId: null, displayName: "Equipment AC");
|
||||
nm.MaterialiseAlarmCondition("alm-ac", "eq-ac", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-ac", parentNodeId: null, displayName: "Equipment AC", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-ac", "eq-ac", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-ac");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnAddComment.ShouldNotBeNull();
|
||||
@@ -168,8 +168,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-s1", parentNodeId: null, displayName: "Equipment S1");
|
||||
nm.MaterialiseAlarmCondition("alm-s1", "eq-s1", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-s1", parentNodeId: null, displayName: "Equipment S1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-s1", "eq-s1", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-s1");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnShelve.ShouldNotBeNull();
|
||||
@@ -196,8 +196,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-s2", parentNodeId: null, displayName: "Equipment S2");
|
||||
nm.MaterialiseAlarmCondition("alm-s2", "eq-s2", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-s2", parentNodeId: null, displayName: "Equipment S2", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-s2", "eq-s2", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-s2");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -228,8 +228,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-s3", parentNodeId: null, displayName: "Equipment S3");
|
||||
nm.MaterialiseAlarmCondition("alm-s3", "eq-s3", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-s3", parentNodeId: null, displayName: "Equipment S3", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-s3", "eq-s3", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-s3");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -254,8 +254,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-s4", parentNodeId: null, displayName: "Equipment S4");
|
||||
nm.MaterialiseAlarmCondition("alm-s4", "eq-s4", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-s4", parentNodeId: null, displayName: "Equipment S4", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-s4", "eq-s4", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-s4");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -287,8 +287,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-tu", parentNodeId: null, displayName: "Equipment TU");
|
||||
nm.MaterialiseAlarmCondition("alm-tu", "eq-tu", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-tu", parentNodeId: null, displayName: "Equipment TU", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-tu", "eq-tu", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-tu");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnTimedUnshelve.ShouldNotBeNull();
|
||||
@@ -322,8 +322,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-ed1", parentNodeId: null, displayName: "Equipment ED1");
|
||||
nm.MaterialiseAlarmCondition("alm-ed1", "eq-ed1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
|
||||
nm.EnsureFolder("eq-ed1", parentNodeId: null, displayName: "Equipment ED1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-ed1", "eq-ed1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-ed1");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnEnableDisable.ShouldNotBeNull();
|
||||
@@ -351,8 +351,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-ed2", parentNodeId: null, displayName: "Equipment ED2");
|
||||
nm.MaterialiseAlarmCondition("alm-ed2", "eq-ed2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
|
||||
nm.EnsureFolder("eq-ed2", parentNodeId: null, displayName: "Equipment ED2", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-ed2", "eq-ed2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-ed2");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnEnableDisable.ShouldNotBeNull();
|
||||
@@ -379,8 +379,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-ed3", parentNodeId: null, displayName: "Equipment ED3");
|
||||
nm.MaterialiseAlarmCondition("alm-ed3", "eq-ed3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
|
||||
nm.EnsureFolder("eq-ed3", parentNodeId: null, displayName: "Equipment ED3", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-ed3", "eq-ed3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-ed3");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -405,8 +405,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var captured = new List<AlarmCommand>();
|
||||
nm.AlarmCommandRouter = captured.Add;
|
||||
|
||||
nm.EnsureFolder("eq-ed4", parentNodeId: null, displayName: "Equipment ED4");
|
||||
nm.MaterialiseAlarmCondition("alm-ed4", "eq-ed4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
|
||||
nm.EnsureFolder("eq-ed4", parentNodeId: null, displayName: "Equipment ED4", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-ed4", "eq-ed4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-ed4");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnEnableDisable.ShouldNotBeNull();
|
||||
@@ -436,8 +436,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak1", parentNodeId: null, displayName: "Equipment NAK1");
|
||||
nm.MaterialiseAlarmCondition("alm-nak1", "eq-nak1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
|
||||
nm.EnsureFolder("eq-nak1", parentNodeId: null, displayName: "Equipment NAK1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-nak1", "eq-nak1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak1");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnAcknowledge.ShouldNotBeNull();
|
||||
@@ -469,8 +469,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak2", parentNodeId: null, displayName: "Equipment NAK2");
|
||||
nm.MaterialiseAlarmCondition("alm-nak2", "eq-nak2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
|
||||
nm.EnsureFolder("eq-nak2", parentNodeId: null, displayName: "Equipment NAK2", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-nak2", "eq-nak2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak2");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnAcknowledge.ShouldNotBeNull();
|
||||
@@ -503,8 +503,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak4", parentNodeId: null, displayName: "Equipment NAK4");
|
||||
nm.MaterialiseAlarmCondition("alm-nak4", "eq-nak4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
|
||||
nm.EnsureFolder("eq-nak4", parentNodeId: null, displayName: "Equipment NAK4", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-nak4", "eq-nak4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak4");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -531,8 +531,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak3", parentNodeId: null, displayName: "Equipment NAK3");
|
||||
nm.MaterialiseAlarmCondition("alm-nak3", "eq-nak3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
|
||||
nm.EnsureFolder("eq-nak3", parentNodeId: null, displayName: "Equipment NAK3", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-nak3", "eq-nak3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak3");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -555,8 +555,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
// No router set (default null).
|
||||
|
||||
nm.EnsureFolder("eq-nr", parentNodeId: null, displayName: "Equipment NR");
|
||||
nm.MaterialiseAlarmCondition("alm-nr", "eq-nr", "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder("eq-nr", parentNodeId: null, displayName: "Equipment NR", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-nr", "eq-nr", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nr");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
@@ -576,8 +576,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
|
||||
|
||||
@@ -591,8 +591,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("a2", "eq", "d", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("a2", "eq", "d", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.IsNativeAlarmNode("a2").ShouldBeFalse();
|
||||
|
||||
@@ -611,15 +611,15 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
|
||||
|
||||
// RebuildAddressSpace clears the folder set too, so the equipment folder must be re-ensured
|
||||
// before the same id can be re-materialised (ResolveParentFolder needs the parent back).
|
||||
nm.RebuildAddressSpace();
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.IsNativeAlarmNode("a1").ShouldBeFalse();
|
||||
|
||||
@@ -636,13 +636,13 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.IsNativeAlarmNode("a1").ShouldBeFalse();
|
||||
|
||||
nm.RebuildAddressSpace();
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
|
||||
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
|
||||
|
||||
|
||||
+11
-10
@@ -1,4 +1,5 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
@@ -27,14 +28,14 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var first = nm.TryGetAlarmCondition("alm-1");
|
||||
first.ShouldNotBeNull();
|
||||
|
||||
// Same id + same kind ⇒ skip-if-present: the existing instance is kept.
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var second = nm.TryGetAlarmCondition("alm-1");
|
||||
|
||||
second.ShouldBeSameAs(first);
|
||||
@@ -50,14 +51,14 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var scripted = nm.TryGetAlarmCondition("alm-1");
|
||||
nm.IsNativeAlarmNode("alm-1").ShouldBeFalse();
|
||||
|
||||
// Same id but the OTHER kind ⇒ recreate (a different instance) and the native flag is now set.
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: true);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
var native = nm.TryGetAlarmCondition("alm-1");
|
||||
|
||||
native.ShouldNotBeSameAs(scripted);
|
||||
@@ -75,16 +76,16 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var before = nm.TryGetAlarmCondition("alm-1");
|
||||
|
||||
nm.RebuildAddressSpace();
|
||||
nm.TryGetAlarmCondition("alm-1").ShouldBeNull();
|
||||
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
var after = nm.TryGetAlarmCondition("alm-1");
|
||||
|
||||
after.ShouldNotBeNull();
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class NodeManagerArrayTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/arr", parentFolderNodeId: null, displayName: "arr", dataType: "Int32",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 8);
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 8, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/arr");
|
||||
variable.ShouldNotBeNull();
|
||||
@@ -52,7 +52,7 @@ public sealed class NodeManagerArrayTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/arr-unfixed", parentFolderNodeId: null, displayName: "arr-unfixed", dataType: "Int32",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: null);
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: null, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/arr-unfixed");
|
||||
variable.ShouldNotBeNull();
|
||||
@@ -72,7 +72,7 @@ public sealed class NodeManagerArrayTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/scalar", parentFolderNodeId: null, displayName: "scalar", dataType: "Int32",
|
||||
writable: false);
|
||||
writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/scalar");
|
||||
variable.ShouldNotBeNull();
|
||||
@@ -91,10 +91,10 @@ public sealed class NodeManagerArrayTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/arrwrite", parentFolderNodeId: null, displayName: "arrwrite", dataType: "Int32",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 3);
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 3, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var payload = new[] { 1, 2, 3 };
|
||||
nm.WriteValue("eq-1/arrwrite", payload, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.WriteValue("eq-1/arrwrite", payload, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/arrwrite");
|
||||
variable.ShouldNotBeNull();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -29,7 +30,7 @@ public sealed class NodeManagerHistorizeTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Tag");
|
||||
writable: false, historianTagname: "WW.Tag", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/temp");
|
||||
variable.ShouldNotBeNull();
|
||||
@@ -56,9 +57,9 @@ public sealed class NodeManagerHistorizeTests : IDisposable
|
||||
|
||||
// Explicit null and the defaulted-param form both mean "not historized".
|
||||
nm.EnsureVariable("eq-1/plain", parentFolderNodeId: null, displayName: "Plain", dataType: "Int32",
|
||||
writable: false, historianTagname: null);
|
||||
writable: false, historianTagname: null, realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-1/plain2", parentFolderNodeId: null, displayName: "Plain2", dataType: "Int32",
|
||||
writable: false);
|
||||
writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
foreach (var nodeId in new[] { "eq-1/plain", "eq-1/plain2" })
|
||||
{
|
||||
@@ -83,7 +84,7 @@ public sealed class NodeManagerHistorizeTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/setpoint", parentFolderNodeId: null, displayName: "Setpoint", dataType: "Float",
|
||||
writable: true, historianTagname: "WW.Setpoint");
|
||||
writable: true, historianTagname: "WW.Setpoint", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/setpoint");
|
||||
variable.ShouldNotBeNull();
|
||||
@@ -109,7 +110,7 @@ public sealed class NodeManagerHistorizeTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Tag");
|
||||
writable: false, historianTagname: "WW.Tag", realm: AddressSpaceRealm.Uns);
|
||||
nm.TryGetHistorizedTagname("eq-1/temp", out _).ShouldBeTrue();
|
||||
|
||||
nm.RebuildAddressSpace();
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
@@ -228,7 +229,7 @@ public sealed class NodeManagerHistoryReadConcurrencyTests : IDisposable
|
||||
{
|
||||
var key = $"eq/tag{i}";
|
||||
nm.EnsureVariable(key, parentFolderNodeId: null, displayName: $"Tag{i}", dataType: "Float",
|
||||
writable: false, historianTagname: $"WW.Tag{i}");
|
||||
writable: false, historianTagname: $"WW.Tag{i}", realm: AddressSpaceRealm.Uns);
|
||||
ids[i] = nm.TryGetVariable(key)!.NodeId;
|
||||
}
|
||||
|
||||
|
||||
+14
-13
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
using Shouldly;
|
||||
@@ -42,8 +43,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
const string equipmentId = "eq-evt";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-1", equipmentId, "HighTemp", "OffNormalAlarm", severity: 700);
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alarm-1", equipmentId, "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
var evtTime = new DateTime(2026, 6, 14, 10, 0, 0, DateTimeKind.Utc);
|
||||
@@ -107,8 +108,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
const string equipmentId = "eq-unbounded";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-0", equipmentId, "Cond", "OffNormalAlarm", severity: 600);
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alarm-0", equipmentId, "Cond", "OffNormalAlarm", severity: 600, realm: AddressSpaceRealm.Uns);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
fake.EventsResult = new HistoricalEventsResult(
|
||||
@@ -143,8 +144,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
const string equipmentId = "eq-unsupported";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-2", equipmentId, "Cond", "OffNormalAlarm", severity: 500);
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alarm-2", equipmentId, "Cond", "OffNormalAlarm", severity: 500, realm: AddressSpaceRealm.Uns);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
fake.EventsResult = new HistoricalEventsResult(
|
||||
@@ -187,8 +188,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
const string equipmentId = "eq-empty";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-3", equipmentId, "Cond", "OffNormalAlarm", severity: 300);
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alarm-3", equipmentId, "Cond", "OffNormalAlarm", severity: 300, realm: AddressSpaceRealm.Uns);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
var details = new ReadEventDetails
|
||||
@@ -221,8 +222,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
// Materialise the alarm while the source is still the Null default — the folder is promoted to
|
||||
// SubscribeToEvents but DOES NOT get the HistoryRead bit / source registration.
|
||||
const string equipmentId = "eq-nosrc";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-4", equipmentId, "Cond", "OffNormalAlarm", severity: 200);
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alarm-4", equipmentId, "Cond", "OffNormalAlarm", severity: 200, realm: AddressSpaceRealm.Uns);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
// Wire a real source AFTER promotion — it must NOT retroactively make the folder a source.
|
||||
@@ -271,7 +272,7 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
// A historized variable node — has AccessLevel.HistoryRead (variable-history reads) but
|
||||
// EventNotifier=None (no event-notifier bit). The SDK base rejects it before our override runs.
|
||||
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Temp");
|
||||
writable: false, historianTagname: "WW.Temp", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/temp")!.NodeId;
|
||||
|
||||
var details = new ReadEventDetails
|
||||
@@ -303,8 +304,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
const string equipmentId = "eq-boom";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-5", equipmentId, "Cond", "OffNormalAlarm", severity: 900);
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alarm-5", equipmentId, "Cond", "OffNormalAlarm", severity: 900, realm: AddressSpaceRealm.Uns);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
var details = new ReadEventDetails
|
||||
|
||||
+9
-8
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
using Shouldly;
|
||||
@@ -42,7 +43,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
|
||||
|
||||
nm.EnsureVariable("eq-1/pv", parentFolderNodeId: null, displayName: "PV", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.PV");
|
||||
writable: false, historianTagname: "WW.PV", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/pv")!.NodeId;
|
||||
|
||||
var collected = new List<double>();
|
||||
@@ -84,7 +85,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(MakeSeries(count: 100, stepSeconds: 1));
|
||||
|
||||
nm.EnsureVariable("eq-1/exact", parentFolderNodeId: null, displayName: "Exact", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.Exact");
|
||||
writable: false, historianTagname: "WW.Exact", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/exact")!.NodeId;
|
||||
|
||||
var (r1, _, cp1) = ReadRaw(nm, nodeId, Epoch, Epoch.AddHours(1), max: 100, inboundCp: null);
|
||||
@@ -123,7 +124,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
|
||||
|
||||
nm.EnsureVariable("eq-1/tie", parentFolderNodeId: null, displayName: "Tie", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.Tie");
|
||||
writable: false, historianTagname: "WW.Tie", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/tie")!.NodeId;
|
||||
|
||||
var collected = new List<double>();
|
||||
@@ -167,7 +168,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
|
||||
|
||||
nm.EnsureVariable("eq-1/burst", parentFolderNodeId: null, displayName: "Burst", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.Burst");
|
||||
writable: false, historianTagname: "WW.Burst", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/burst")!.NodeId;
|
||||
|
||||
var collected = new List<double>();
|
||||
@@ -211,7 +212,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
|
||||
|
||||
nm.EnsureVariable("eq-1/absurd", parentFolderNodeId: null, displayName: "Absurd", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.Absurd");
|
||||
writable: false, historianTagname: "WW.Absurd", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/absurd")!.NodeId;
|
||||
|
||||
// Page 1: a full page of the first 2 ties, with a continuation point.
|
||||
@@ -241,7 +242,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = new SeriesHistorianDataSource(MakeSeries(count: 250, stepSeconds: 1));
|
||||
|
||||
nm.EnsureVariable("eq-1/all", parentFolderNodeId: null, displayName: "All", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.All");
|
||||
writable: false, historianTagname: "WW.All", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/all")!.NodeId;
|
||||
|
||||
var (r, e, cp) = ReadRaw(nm, nodeId, Epoch, Epoch.AddHours(1), max: 0, inboundCp: null);
|
||||
@@ -265,7 +266,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/bad-cp", parentFolderNodeId: null, displayName: "BadCp", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.BadCp");
|
||||
writable: false, historianTagname: "WW.BadCp", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/bad-cp")!.NodeId;
|
||||
|
||||
fake.ResetReadCount();
|
||||
@@ -291,7 +292,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/rel", parentFolderNodeId: null, displayName: "Rel", dataType: "Double",
|
||||
writable: false, historianTagname: "WW.Rel");
|
||||
writable: false, historianTagname: "WW.Rel", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/rel")!.NodeId;
|
||||
|
||||
// Page 1 — get a CP.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
using Shouldly;
|
||||
@@ -39,7 +40,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Temp");
|
||||
writable: false, historianTagname: "WW.Temp", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/temp")!.NodeId;
|
||||
|
||||
var src = DateTime.UtcNow.AddSeconds(-5);
|
||||
@@ -91,7 +92,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
|
||||
// Node id "eq-9/flow" but a DISTINCT historian tagname "Plant.Flow.PV".
|
||||
nm.EnsureVariable("eq-9/flow", parentFolderNodeId: null, displayName: "Flow", dataType: "Double",
|
||||
writable: false, historianTagname: "Plant.Flow.PV");
|
||||
writable: false, historianTagname: "Plant.Flow.PV", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-9/flow")!.NodeId;
|
||||
|
||||
var details = new ReadRawModifiedDetails
|
||||
@@ -123,7 +124,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/empty", parentFolderNodeId: null, displayName: "Empty", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Empty");
|
||||
writable: false, historianTagname: "WW.Empty", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/empty")!.NodeId;
|
||||
|
||||
var details = new ReadRawModifiedDetails
|
||||
@@ -156,7 +157,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
|
||||
// Plain (non-historized) variable — no HistoryRead access bit.
|
||||
nm.EnsureVariable("eq-1/plain", parentFolderNodeId: null, displayName: "Plain", dataType: "Int32",
|
||||
writable: false, historianTagname: null);
|
||||
writable: false, historianTagname: null, realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/plain")!.NodeId;
|
||||
|
||||
var details = new ReadRawModifiedDetails
|
||||
@@ -185,7 +186,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/mod", parentFolderNodeId: null, displayName: "Mod", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Mod");
|
||||
writable: false, historianTagname: "WW.Mod", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/mod")!.NodeId;
|
||||
|
||||
var details = new ReadRawModifiedDetails
|
||||
@@ -215,7 +216,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/avg", parentFolderNodeId: null, displayName: "Avg", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Avg");
|
||||
writable: false, historianTagname: "WW.Avg", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/avg")!.NodeId;
|
||||
|
||||
var details = new ReadProcessedDetails
|
||||
@@ -247,7 +248,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/sd", parentFolderNodeId: null, displayName: "Sd", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Sd");
|
||||
writable: false, historianTagname: "WW.Sd", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/sd")!.NodeId;
|
||||
|
||||
var details = new ReadProcessedDetails
|
||||
@@ -277,7 +278,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/at", parentFolderNodeId: null, displayName: "At", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.At");
|
||||
writable: false, historianTagname: "WW.At", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/at")!.NodeId;
|
||||
|
||||
var t1 = DateTime.UtcNow.AddMinutes(-2);
|
||||
@@ -309,9 +310,9 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
|
||||
// Materialise two distinct historized variables under separate equipment folders.
|
||||
nm.EnsureVariable("eqA/good", parentFolderNodeId: null, displayName: "Good", dataType: "Float",
|
||||
writable: false, historianTagname: "A.PV");
|
||||
writable: false, historianTagname: "A.PV", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eqB/bad", parentFolderNodeId: null, displayName: "Bad", dataType: "Float",
|
||||
writable: false, historianTagname: "B.PV");
|
||||
writable: false, historianTagname: "B.PV", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var goodNodeId = nm.TryGetVariable("eqA/good")!.NodeId;
|
||||
var badNodeId = nm.TryGetVariable("eqB/bad")!.NodeId;
|
||||
@@ -368,7 +369,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
nm.EnsureVariable("eq-1/boom", parentFolderNodeId: null, displayName: "Boom", dataType: "Float",
|
||||
writable: false, historianTagname: "WW.Boom");
|
||||
writable: false, historianTagname: "WW.Boom", realm: AddressSpaceRealm.Uns);
|
||||
var nodeId = nm.TryGetVariable("eq-1/boom")!.NodeId;
|
||||
|
||||
var details = new ReadRawModifiedDetails
|
||||
|
||||
+6
-6
@@ -43,8 +43,8 @@ public sealed class NodeManagerModelChangeOnAddTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureFolder("eq-7", parentNodeId: null, displayName: "Equipment 7");
|
||||
nm.EnsureVariable("eq-7/speed", parentFolderNodeId: "eq-7", displayName: "Speed", dataType: "Float", writable: false);
|
||||
nm.EnsureFolder("eq-7", parentNodeId: null, displayName: "Equipment 7", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-7/speed", parentFolderNodeId: "eq-7", displayName: "Speed", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
var parent = nm.TryGetFolder("eq-7")!;
|
||||
|
||||
var e = nm.BuildNodesAddedModelChange("eq-7");
|
||||
@@ -95,13 +95,13 @@ public sealed class NodeManagerModelChangeOnAddTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
// Before any nodes exist under the parent — must not throw.
|
||||
Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9"));
|
||||
Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9", realm: AddressSpaceRealm.Uns));
|
||||
|
||||
nm.EnsureFolder("eq-9", parentNodeId: null, displayName: "Equipment 9");
|
||||
nm.EnsureVariable("eq-9/temp", parentFolderNodeId: "eq-9", displayName: "Temp", dataType: "Float", writable: false);
|
||||
nm.EnsureFolder("eq-9", parentNodeId: null, displayName: "Equipment 9", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-9/temp", parentFolderNodeId: "eq-9", displayName: "Temp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
// After the nodes are materialised — still must not throw.
|
||||
Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9"));
|
||||
Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9", realm: AddressSpaceRealm.Uns));
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
|
||||
try
|
||||
{
|
||||
var ex = Should.Throw<InvalidOperationException>(() =>
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment"));
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns));
|
||||
ex.Message.ShouldContain("address space has not been created");
|
||||
}
|
||||
finally
|
||||
@@ -55,7 +55,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp",
|
||||
dataType: "Float", writable: false));
|
||||
dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -70,7 +70,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
|
||||
try
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
nm.WriteValue("eq-1/temp", 1.0, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
nm.WriteValue("eq-1/temp", 1.0, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
|
||||
try
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
nm.MaterialiseAlarmCondition("alarm-1", "eq-1", "Cond", "OffNormalAlarm", severity: 500));
|
||||
nm.MaterialiseAlarmCondition("alarm-1", "eq-1", "Cond", "OffNormalAlarm", severity: 500, realm: AddressSpaceRealm.Uns));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
+25
-24
@@ -1,4 +1,5 @@
|
||||
using Opc.Ua;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
@@ -31,13 +32,13 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A");
|
||||
nm.EnsureVariable("eq-1/B", "eq-1", "B", "Float", writable: false);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-1/B", "eq-1", "B", "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
var countBefore = nm.VariableCount;
|
||||
nm.TryGetHistorizedTagname("eq-1/A", out _).ShouldBeTrue();
|
||||
|
||||
nm.RemoveVariableNode("eq-1/A").ShouldBeTrue();
|
||||
nm.RemoveVariableNode("eq-1/A", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
|
||||
nm.TryGetVariable("eq-1/A").ShouldBeNull();
|
||||
nm.VariableCount.ShouldBe(countBefore - 1);
|
||||
@@ -56,7 +57,7 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.RemoveVariableNode("eq-1/nope").ShouldBeFalse();
|
||||
nm.RemoveVariableNode("eq-1/nope", realm: AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -68,8 +69,8 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var e = nm.BuildNodesRemovedModelChange("eq-1/A");
|
||||
|
||||
@@ -92,17 +93,17 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldNotBeNull();
|
||||
nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeTrue();
|
||||
|
||||
nm.RemoveAlarmConditionNode("eq-1/OverTemp").ShouldBeTrue();
|
||||
nm.RemoveAlarmConditionNode("eq-1/OverTemp", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
|
||||
nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldBeNull();
|
||||
nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeFalse();
|
||||
|
||||
nm.RemoveAlarmConditionNode("eq-1/OverTemp").ShouldBeFalse(); // already gone ⇒ false
|
||||
nm.RemoveAlarmConditionNode("eq-1/OverTemp", realm: AddressSpaceRealm.Uns).ShouldBeFalse(); // already gone ⇒ false
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -114,10 +115,10 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 500, isNative: false);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 500, isNative: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.RemoveAlarmConditionNode("alm-1").ShouldBeTrue();
|
||||
nm.RemoveAlarmConditionNode("alm-1", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
nm.TryGetAlarmCondition("alm-1").ShouldBeNull();
|
||||
|
||||
await host.DisposeAsync();
|
||||
@@ -136,17 +137,17 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
// Target equipment eq-1 with a sub-folder, two variables (one historized), and a native condition.
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
|
||||
nm.EnsureFolder("eq-1/Diag", parentNodeId: "eq-1", displayName: "Diag");
|
||||
nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A");
|
||||
nm.EnsureVariable("eq-1/Diag/T", "eq-1/Diag", "T", "Float", writable: false);
|
||||
nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true);
|
||||
nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureFolder("eq-1/Diag", parentNodeId: "eq-1", displayName: "Diag", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-1/Diag/T", "eq-1/Diag", "T", "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
// Sibling equipment eq-2 that must survive untouched.
|
||||
nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2");
|
||||
nm.EnsureVariable("eq-2/S", "eq-2", "S", "Float", writable: false);
|
||||
nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2", realm: AddressSpaceRealm.Uns);
|
||||
nm.EnsureVariable("eq-2/S", "eq-2", "S", "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.RemoveEquipmentSubtree("eq-1").ShouldBeTrue();
|
||||
nm.RemoveEquipmentSubtree("eq-1", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
|
||||
|
||||
// Every eq-1 descendant is gone from every map.
|
||||
nm.TryGetFolder("eq-1").ShouldBeNull();
|
||||
@@ -163,7 +164,7 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
|
||||
// Re-materialising an alarm under eq-2 still works (the notifier machinery was not corrupted by the
|
||||
// eq-1 demotion) — proves no orphaned root-notifier ref broke the event path.
|
||||
Should.NotThrow(() => nm.MaterialiseAlarmCondition("eq-2/Alm", "eq-2", "Alm", "OffNormalAlarm", 300, isNative: false));
|
||||
Should.NotThrow(() => nm.MaterialiseAlarmCondition("eq-2/Alm", "eq-2", "Alm", "OffNormalAlarm", 300, isNative: false, realm: AddressSpaceRealm.Uns));
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -176,7 +177,7 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.RemoveEquipmentSubtree("eq-nope").ShouldBeFalse();
|
||||
nm.RemoveEquipmentSubtree("eq-nope", realm: AddressSpaceRealm.Uns).ShouldBeFalse();
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
+19
-19
@@ -44,13 +44,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false);
|
||||
nm.WriteValue("eq-1/sp", 3.5f, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 3.5f, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.DataType.ShouldBe(DataTypeIds.Float); // arrange guard
|
||||
|
||||
var applied = nm.UpdateTagAttributes("eq-1/sp", writable: false, historianTagname: null,
|
||||
dataType: "Int32", isArray: false, arrayLength: null);
|
||||
dataType: "Int32", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
applied.ShouldBeTrue();
|
||||
node.DataType.ShouldBe(DataTypeIds.Int32); // swapped in place
|
||||
@@ -69,13 +69,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false);
|
||||
nm.WriteValue("eq-1/buf", (short)42, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", (short)42, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
node.ValueRank.ShouldBe(ValueRanks.Scalar); // arrange guard
|
||||
|
||||
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
|
||||
dataType: "Int16", isArray: true, arrayLength: 8u);
|
||||
dataType: "Int16", isArray: true, arrayLength: 8u, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
applied.ShouldBeTrue();
|
||||
node.ValueRank.ShouldBe(ValueRanks.OneDimension);
|
||||
@@ -96,13 +96,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 4u);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 4u, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
node.ArrayDimensions![0].ShouldBe(4u); // arrange guard
|
||||
|
||||
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
|
||||
dataType: "Int16", isArray: true, arrayLength: 8u);
|
||||
dataType: "Int16", isArray: true, arrayLength: 8u, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
applied.ShouldBeTrue();
|
||||
node.ArrayDimensions[0].ShouldBe(8u);
|
||||
@@ -121,13 +121,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 4u);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 4u, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
node.ValueRank.ShouldBe(ValueRanks.OneDimension); // arrange guard
|
||||
|
||||
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
|
||||
dataType: "Int16", isArray: false, arrayLength: null);
|
||||
dataType: "Int16", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
applied.ShouldBeTrue();
|
||||
node.ValueRank.ShouldBe(ValueRanks.Scalar);
|
||||
@@ -146,11 +146,11 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false);
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
|
||||
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
|
||||
dataType: "Int16", isArray: true, arrayLength: null);
|
||||
dataType: "Int16", isArray: true, arrayLength: null, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
applied.ShouldBeTrue();
|
||||
node.ValueRank.ShouldBe(ValueRanks.OneDimension);
|
||||
@@ -171,13 +171,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false);
|
||||
nm.WriteValue("eq-1/sp", 7.0f, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7.0f, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
// Same DataType ("Float") + same scalar shape — only Writable flips false → true.
|
||||
var applied = nm.UpdateTagAttributes("eq-1/sp", writable: true, historianTagname: null,
|
||||
dataType: "Float", isArray: false, arrayLength: null);
|
||||
dataType: "Float", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
applied.ShouldBeTrue();
|
||||
node.Value.ShouldBe(7.0f); // value preserved (NOT reset)
|
||||
@@ -199,7 +199,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
|
||||
bool result = true;
|
||||
Should.NotThrow(() => result = nm.UpdateTagAttributes("eq-1/gone", writable: false, historianTagname: null,
|
||||
dataType: "Int32", isArray: false, arrayLength: null));
|
||||
dataType: "Int32", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns));
|
||||
result.ShouldBeFalse();
|
||||
|
||||
await host.DisposeAsync();
|
||||
@@ -215,7 +215,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
var e = nm.BuildNodeShapeChangedEvent(node);
|
||||
|
||||
@@ -50,9 +50,9 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
// Prior state = Good 7; the SDK then optimistically applied 42 (the write the device will reject).
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.Value = 42; // simulate the SDK's optimistic apply of the rejected write
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
optimisticValue: 42,
|
||||
priorValue: 7,
|
||||
priorStatus: StatusCodes.Good,
|
||||
clientUserId: "op");
|
||||
clientUserId: "op", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
node.Value.ShouldBe(7); // settled back to prior value
|
||||
node.StatusCode.ShouldBe((StatusCode)StatusCodes.Good); // settled back to prior status (NOT stuck Bad)
|
||||
@@ -78,15 +78,15 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.Value = 42;
|
||||
|
||||
nm.RevertOptimisticWriteIfNeeded(
|
||||
"eq-1/sp",
|
||||
outcome: new NodeWriteOutcome(true, null),
|
||||
optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op");
|
||||
optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
node.Value.ShouldBe(42); // success ⇒ optimistic value stands
|
||||
|
||||
@@ -101,15 +101,15 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.Value = 99; // a fresh poll already republished the confirmed register value (NOT the optimistic 42)
|
||||
|
||||
nm.RevertOptimisticWriteIfNeeded(
|
||||
"eq-1/sp",
|
||||
outcome: new NodeWriteOutcome(false, "device rejected"),
|
||||
optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op");
|
||||
optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op", realm: AddressSpaceRealm.Uns);
|
||||
|
||||
node.Value.ShouldBe(99); // poll value preserved — not reverted to 7
|
||||
|
||||
@@ -127,7 +127,71 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
Should.NotThrow(() => nm.RevertOptimisticWriteIfNeeded(
|
||||
"eq-1/gone",
|
||||
outcome: new NodeWriteOutcome(false, "device rejected"),
|
||||
optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op"));
|
||||
optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op", realm: AddressSpaceRealm.Uns));
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
// ─────────────────── v3 Batch 4 (review M2) — realm-qualified dual-node self-correction ───────────────────
|
||||
|
||||
/// <summary>(M2a) A failed write driven through a UNS NodeId reverts ONLY the UNS node back to its prior
|
||||
/// value; the raw node that shares the SAME bare id (but lives in the Raw realm) is NEVER touched and keeps
|
||||
/// its driver value. The revert routes by (realm, bareId), so it can't clobber the sibling node.</summary>
|
||||
[Fact]
|
||||
public async Task Failed_uns_write_reverts_uns_node_and_leaves_raw_node_untouched()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
// Same bare id "A/B/C/D" in BOTH realms — the raw device node and the UNS reference node.
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Raw", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Raw);
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Uns", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
var now = DateTime.UtcNow;
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Raw); // raw driver value
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Uns); // fanned UNS value
|
||||
var raw = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Raw)!;
|
||||
var uns = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Uns)!;
|
||||
|
||||
uns.Value = 42; // the SDK optimistically applied the client's UNS write
|
||||
|
||||
nm.RevertOptimisticWriteIfNeeded(
|
||||
"A/B/C/D", outcome: new NodeWriteOutcome(false, "device rejected"),
|
||||
optimisticValue: 42, priorValue: 10, priorStatus: StatusCodes.Good, clientUserId: "op",
|
||||
realm: AddressSpaceRealm.Uns);
|
||||
|
||||
uns.Value.ShouldBe(10); // UNS node reverted to prior
|
||||
raw.Value.ShouldBe(10); // raw node NEVER changed (realm-qualified revert)
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>(M2b) A raw-realm revert reverts the RAW node and leaves the same-bare-id UNS node untouched.
|
||||
/// This locks the realm arg: were it omitted (defaulting to Uns) the revert would target the UNS node — whose
|
||||
/// value is not the optimistic one, so the still-holds-optimistic guard skips it — leaving the raw node stuck
|
||||
/// at the rejected value and FAILING the raw-reverts assertion below.</summary>
|
||||
[Fact]
|
||||
public async Task Raw_realm_revert_reverts_raw_node_only()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Raw", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Raw);
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Uns", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
var now = DateTime.UtcNow;
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Raw);
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Uns);
|
||||
var raw = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Raw)!;
|
||||
var uns = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Uns)!;
|
||||
|
||||
raw.Value = 42; // optimistic write to the RAW node
|
||||
|
||||
nm.RevertOptimisticWriteIfNeeded(
|
||||
"A/B/C/D", outcome: new NodeWriteOutcome(false, "device rejected"),
|
||||
optimisticValue: 42, priorValue: 10, priorStatus: StatusCodes.Good, clientUserId: "op",
|
||||
realm: AddressSpaceRealm.Raw);
|
||||
|
||||
raw.Value.ShouldBe(10); // raw reverted (would stay 42 if the realm were dropped to Uns)
|
||||
uns.Value.ShouldBe(10); // UNS node untouched
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
@@ -143,8 +207,8 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
var audit = nm.BuildWriteFailureAuditEvent(
|
||||
@@ -174,8 +238,8 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
var audit = nm.BuildWriteFailureAuditEvent(
|
||||
|
||||
Reference in New Issue
Block a user