fix(v3-batch4-wp2): AddReference sink seam for Organizes UNS→Raw (Wave A review M1)
Close the Wave-A M1 gap: the sink surface had no way to wire the mandated cross-tree Organizes reference from each UNS reference Variable to its backing Raw node, forcing WP3 to reopen the frozen surface. Add a dedicated realm-qualified AddReference method instead. - IOpcUaAddressSpaceSink.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType="Organizes"): realm-qualified both ends; idempotent; a missing endpoint is a logged no-op (never throws) so a mid-rebuild race can't fault a deploy. Base-interface capability (no surgical sniff, no transitional default — WP3 wires it explicitly per UNS reference variable). - SdkAddressSpaceSink forwards to the node manager; DeferredAddressSpaceSink forwards unconditionally (like EnsureFolder); NullOpcUaAddressSpaceSink no-ops. - OtOpcUaNodeManager.AddReference: resolve both nodes by full ns-qualified key (variable/folder/condition via ResolveNodeState), guard both-exist, then wire the edge bidirectionally (forward Organizes on source, inverse on target) with a ReferenceExists idempotency guard + ClearChangeMasks on mutated sides. Reference type resolved by ResolveReferenceType (Organizes default). Added internal GetNodeReferences test/diagnostic accessor. - Reflection guard: the exhaustive-forwarding test + the realm-discriminator guard auto-cover AddReference (it has two AddressSpaceRealm params) — no edit needed. - New SdkAddressSpaceSinkTests: Organizes UNS→Raw edge created bidirectionally + idempotent; missing endpoint is a safe no-op. - All 15 IOpcUaAddressSpaceSink test doubles gain the AddReference no-op so the solution still builds. Build: dotnet build ZB.MOM.WW.OtOpcUa.slnx = 0 errors. Tests: Commons.Tests 310/310; OpcUaServer.Tests 337 passed / 4 pre-existing skips. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -48,6 +48,10 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes")
|
||||
=> _inner.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType);
|
||||
|
||||
/// <inheritdoc />
|
||||
// Forwards to the inner sink when it supports the surgical capability; returns false otherwise —
|
||||
// before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner
|
||||
|
||||
@@ -111,6 +111,26 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <param name="realm">The namespace realm <paramref name="affectedNodeId"/> lives in.</param>
|
||||
// transitional default — B4-WP3/Wave B makes every call site explicit and removes this default.
|
||||
void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns);
|
||||
|
||||
/// <summary>
|
||||
/// Add an OPC UA reference from an already-materialised source node to an already-materialised
|
||||
/// target node, both realm-qualified. Used by <c>AddressSpaceApplier</c> to link each UNS reference
|
||||
/// Variable (source, <see cref="AddressSpaceRealm.Uns"/>) to its backing Raw node (target,
|
||||
/// <see cref="AddressSpaceRealm.Raw"/>) with an <c>Organizes</c> edge, so the UNS variable
|
||||
/// browse-resolves to its raw source (and the raw node back via the inverse). The edge is wired
|
||||
/// bidirectionally (forward on the source, inverse on the target) so browse resolves both ways.
|
||||
/// <b>Idempotent</b> (an existing edge is not duplicated); a <b>missing endpoint is a no-op</b>
|
||||
/// (logged, never thrown) so a mid-rebuild race can't fault a deploy.
|
||||
/// </summary>
|
||||
/// <param name="sourceNodeId">The source node's <c>s=</c> id (the referencing node — e.g. the UNS variable).</param>
|
||||
/// <param name="sourceRealm">The namespace realm <paramref name="sourceNodeId"/> lives in.</param>
|
||||
/// <param name="targetNodeId">The target node's <c>s=</c> id (the referenced node — e.g. the backing Raw node).</param>
|
||||
/// <param name="targetRealm">The namespace realm <paramref name="targetNodeId"/> lives in.</param>
|
||||
/// <param name="referenceType">The hierarchical reference type name — <c>Organizes</c> (default),
|
||||
/// <c>HasComponent</c>, or <c>HasProperty</c>; unknown names fall back to <c>Organizes</c>.</param>
|
||||
void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm,
|
||||
string targetNodeId, AddressSpaceRealm targetRealm,
|
||||
string referenceType = "Organizes");
|
||||
}
|
||||
|
||||
/// <summary>OPC UA status code projection — Good / Uncertain / Bad. Real SDK has finer-grained
|
||||
@@ -144,4 +164,7 @@ public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
@@ -1736,6 +1736,97 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IOpcUaAddressSpaceSink.AddReference" />
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm,
|
||||
string targetNodeId, AddressSpaceRealm targetRealm,
|
||||
string referenceType = "Organizes")
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(sourceNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(targetNodeId);
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
var refType = ResolveReferenceType(referenceType);
|
||||
var sourceKey = MapKey(sourceRealm, sourceNodeId);
|
||||
var targetKey = MapKey(targetRealm, targetNodeId);
|
||||
lock (Lock)
|
||||
{
|
||||
var source = ResolveNodeState(sourceKey);
|
||||
var target = ResolveNodeState(targetKey);
|
||||
// A missing endpoint is a no-op (logged) — the applier may drive the cross-tree link while a
|
||||
// concurrent rebuild has torn one side down; never throw out of a deploy-time reference wire.
|
||||
if (source is null || target is null)
|
||||
{
|
||||
#pragma warning disable CS0618 // Utils.LogWarning is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry.
|
||||
Utils.LogWarning(
|
||||
"OtOpcUaNodeManager: AddReference({0}) skipped — {1} node missing (source '{2}' present={3}, target '{4}' present={5}).",
|
||||
refType, source is null ? "source" : "target", sourceKey, source is not null, targetKey, target is not null);
|
||||
#pragma warning restore CS0618
|
||||
return;
|
||||
}
|
||||
|
||||
// Idempotent: a re-apply must not duplicate the edge. Organizes is hierarchical, so wire it
|
||||
// bidirectionally — forward on the source, inverse (OrganizedBy) on the target — so browse
|
||||
// resolves both ways. Only ClearChangeMasks a side we actually mutated.
|
||||
var sourceChanged = false;
|
||||
var targetChanged = false;
|
||||
if (!source.ReferenceExists(refType, isInverse: false, target.NodeId))
|
||||
{
|
||||
source.AddReference(refType, isInverse: false, target.NodeId);
|
||||
sourceChanged = true;
|
||||
}
|
||||
if (!target.ReferenceExists(refType, isInverse: true, source.NodeId))
|
||||
{
|
||||
target.AddReference(refType, isInverse: true, source.NodeId);
|
||||
targetChanged = true;
|
||||
}
|
||||
if (sourceChanged) source.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
if (targetChanged) target.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolve a materialised node (variable, folder, or alarm condition) by its full
|
||||
/// (namespace-qualified) map key, or null when no node is registered under that key. Used by
|
||||
/// <see cref="AddReference"/> to link two already-materialised nodes regardless of their kind.</summary>
|
||||
/// <param name="key">The namespace-qualified map key (see <see cref="MapKey(AddressSpaceRealm, string)"/>).</param>
|
||||
/// <returns>The node state, or null when unknown.</returns>
|
||||
private NodeState? ResolveNodeState(string key)
|
||||
{
|
||||
if (_variables.TryGetValue(key, out var variable)) return variable;
|
||||
if (_folders.TryGetValue(key, out var folder)) return folder;
|
||||
if (_alarmConditions.TryGetValue(key, out var condition)) return condition;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Test/diagnostic accessor: snapshot the references on a materialised node (by realm-qualified
|
||||
/// id) using the manager's own <c>SystemContext</c>, or an empty list when the node is unknown. Exposed
|
||||
/// so <c>SdkAddressSpaceSinkTests</c> can assert the cross-tree <c>Organizes</c> edge
|
||||
/// <see cref="AddReference"/> wires without needing the (protected) SDK system context.</summary>
|
||||
/// <param name="nodeId">The node's <c>s=</c> id.</param>
|
||||
/// <param name="realm">The realm the node lives in.</param>
|
||||
/// <returns>The node's references, or an empty list when the node is not registered.</returns>
|
||||
internal IReadOnlyList<IReference> GetNodeReferences(string nodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
var node = ResolveNodeState(MapKey(realm, nodeId));
|
||||
if (node is null) return Array.Empty<IReference>();
|
||||
var list = new List<IReference>();
|
||||
node.GetReferences(SystemContext, list);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Map a hierarchical reference-type name to its SDK <see cref="NodeId"/>. Unknown names fall
|
||||
/// back to <see cref="ReferenceTypeIds.Organizes"/> (the UNS→Raw cross-tree link).</summary>
|
||||
/// <param name="referenceType">The reference-type name (e.g. "Organizes").</param>
|
||||
/// <returns>The reference-type NodeId.</returns>
|
||||
private static NodeId ResolveReferenceType(string referenceType) => referenceType switch
|
||||
{
|
||||
"HasComponent" => ReferenceTypeIds.HasComponent,
|
||||
"HasProperty" => ReferenceTypeIds.HasProperty,
|
||||
_ => ReferenceTypeIds.Organizes,
|
||||
};
|
||||
|
||||
/// <summary>Build (but do not report) the Part 3 <c>GeneralModelChangeEvent</c> announcing that nodes were
|
||||
/// added under <paramref name="affectedNodeId"/>. MIRRORS <see cref="BuildNodeShapeChangedEvent"/> exactly —
|
||||
/// the only differences are <c>Verb = NodeAdded</c> (vs <c>DataTypeChanged</c>) and <c>Affected</c> = the
|
||||
|
||||
@@ -65,4 +65,8 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes")
|
||||
=> _nodeManager.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
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 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") { }
|
||||
}
|
||||
|
||||
private sealed class SpySurgicalSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
|
||||
@@ -209,6 +210,7 @@ public class DeferredAddressSpaceSinkTests
|
||||
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 RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
|
||||
+1
@@ -189,5 +189,6 @@ public sealed class AddressSpaceApplierFailureSurfaceTests
|
||||
}
|
||||
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,5 +338,6 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
|
||||
public void RebuildAddressSpace() { }
|
||||
/// <summary>Announces a NodeAdded model-change (stub implementation for testing).</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2307,6 +2307,7 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
/// <summary>A recording sink that does NOT implement <see cref="ISurgicalAddressSpaceSink"/> — used to
|
||||
@@ -2331,6 +2332,7 @@ public sealed class AddressSpaceApplierTests
|
||||
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
|
||||
/// <summary>No-op NodeAdded model-change announcement.</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
private sealed class ThrowingSink : IOpcUaAddressSpaceSink
|
||||
@@ -2379,5 +2381,6 @@ public sealed class AddressSpaceApplierTests
|
||||
public void RebuildAddressSpace() { }
|
||||
/// <summary>No-op NodeAdded model-change announcement.</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +249,7 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public void RebuildAddressSpace() => CallQueue.Enqueue("RB");
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => CallQueue.Enqueue($"NA:{affectedNodeId}");
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
private sealed class SurgicalRecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
|
||||
@@ -298,5 +299,6 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public void RebuildAddressSpace() { }
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,67 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>B4-WP2 M1: AddReference wires the cross-tree Organizes edge from a UNS reference variable
|
||||
/// (source, realm=Uns) to its backing Raw node (target, realm=Raw), bidirectionally, so browse resolves
|
||||
/// both ways — and is idempotent.</summary>
|
||||
[Fact]
|
||||
public async Task AddReference_wires_Organizes_edge_from_UNS_variable_to_Raw_node()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
var sink = new SdkAddressSpaceSink(nm);
|
||||
|
||||
// Backing Raw node (device tree) and the UNS reference variable (equipment tree).
|
||||
sink.EnsureVariable("MAIN/modbus/dev/grp/temp", parentFolderNodeId: null, displayName: "Temp",
|
||||
dataType: "Float", writable: false, realm: AddressSpaceRealm.Raw);
|
||||
sink.EnsureVariable("Area/Line/Eq/Temperature", parentFolderNodeId: null, displayName: "Temperature",
|
||||
dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
sink.AddReference("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns,
|
||||
"MAIN/modbus/dev/grp/temp", AddressSpaceRealm.Raw);
|
||||
|
||||
var unsVar = nm.TryGetVariable("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns);
|
||||
var rawVar = nm.TryGetVariable("MAIN/modbus/dev/grp/temp", AddressSpaceRealm.Raw);
|
||||
unsVar.ShouldNotBeNull();
|
||||
rawVar.ShouldNotBeNull();
|
||||
|
||||
// Forward Organizes on the UNS variable → Raw node; inverse (OrganizedBy) on the Raw node.
|
||||
unsVar.ReferenceExists(ReferenceTypeIds.Organizes, isInverse: false, rawVar.NodeId).ShouldBeTrue();
|
||||
rawVar.ReferenceExists(ReferenceTypeIds.Organizes, isInverse: true, unsVar.NodeId).ShouldBeTrue();
|
||||
|
||||
// Idempotent: a second identical call does not duplicate the edge. The UNS variable's only
|
||||
// FORWARD Organizes ref is the one we added (its parent link manifests as an inverse ref).
|
||||
sink.AddReference("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns,
|
||||
"MAIN/modbus/dev/grp/temp", AddressSpaceRealm.Raw);
|
||||
nm.GetNodeReferences("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns)
|
||||
.Count(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse).ShouldBe(1);
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>B4-WP2 M1: a missing endpoint makes AddReference a safe no-op (never throws, no edge added).</summary>
|
||||
[Fact]
|
||||
public async Task AddReference_with_missing_endpoint_is_a_safe_no_op()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
var sink = new SdkAddressSpaceSink(nm);
|
||||
|
||||
sink.EnsureVariable("Area/Line/Eq/Temperature", parentFolderNodeId: null, displayName: "Temperature",
|
||||
dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
// Target does not exist ⇒ no-op, no throw.
|
||||
Should.NotThrow(() => sink.AddReference("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns,
|
||||
"MAIN/does/not/exist", AddressSpaceRealm.Raw));
|
||||
|
||||
var unsVar = nm.TryGetVariable("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns);
|
||||
unsVar.ShouldNotBeNull();
|
||||
nm.GetNodeReferences("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns)
|
||||
.Any(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse).ShouldBeFalse();
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RebuildAddressSpace clears all registered variables.</summary>
|
||||
[Fact]
|
||||
public async Task RebuildAddressSpace_clears_all_registered_variables()
|
||||
|
||||
+1
@@ -346,5 +346,6 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase
|
||||
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _modelChanges.Enqueue(affectedNodeId);
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -224,5 +224,6 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
|
||||
public void RebuildAddressSpace() { /* recorded via span */ }
|
||||
/// <summary>Announces a NodeAdded model-change (stub implementation).</summary>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -125,6 +125,7 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
|
||||
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 RebuildAddressSpace() => throw new InvalidOperationException("simulated rebuild fault");
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
/// <summary>A no-op sink — a clean apply (no failures).</summary>
|
||||
@@ -137,6 +138,7 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
|
||||
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 RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
/// <summary>Listens to a single instrument by name on the central meter and tallies value + tags.</summary>
|
||||
|
||||
@@ -473,6 +473,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Calls.Enqueue($"NA:{affectedNodeId}");
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
/// <summary>Records a surgical in-place tag-attribute update (always succeeds in this recording sink).</summary>
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
|
||||
@@ -647,6 +647,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
/// <summary>Test implementation of IServiceLevelPublisher that records publishes.</summary>
|
||||
|
||||
Reference in New Issue
Block a user