using System.Collections.Concurrent; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; public sealed class DeferredAddressSpaceSinkTests { /// Verifies that the default inner is a null sink so calls before SetSink are safe. [Fact] public void Default_inner_is_null_sink_so_calls_before_SetSink_are_safe() { var deferred = new DeferredAddressSpaceSink(); // No throw, no observable side effect. deferred.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow); deferred.WriteAlarmState("a", true, false, DateTime.UtcNow); deferred.RebuildAddressSpace(); } /// Verifies that calls after SetSink are forwarded to the inner sink. [Fact] public void Calls_after_SetSink_are_forwarded_to_the_inner() { var deferred = new DeferredAddressSpaceSink(); var inner = new RecordingSink(); deferred.SetSink(inner); deferred.WriteValue("x", 42, OpcUaQuality.Good, DateTime.UtcNow); deferred.WriteAlarmState("a-1", true, false, DateTime.UtcNow); deferred.RebuildAddressSpace(); inner.Calls.ShouldBe(new[] { "WV:x", "WA:a-1", "RB" }); } /// Verifies that setting sink to null reverts to null sink. [Fact] public void SetSink_to_null_reverts_to_null_sink() { var deferred = new DeferredAddressSpaceSink(); var inner = new RecordingSink(); deferred.SetSink(inner); deferred.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow); inner.Calls.Count.ShouldBe(1); deferred.SetSink(null); deferred.WriteValue("y", 2, OpcUaQuality.Good, DateTime.UtcNow); // dropped inner.Calls.Count.ShouldBe(1); } /// Verifies that sink can be swapped between implementations. [Fact] public void SetSink_can_swap_between_implementations() { var deferred = new DeferredAddressSpaceSink(); var first = new RecordingSink(); var second = new RecordingSink(); deferred.SetSink(first); deferred.WriteValue("a", 1, OpcUaQuality.Good, DateTime.UtcNow); deferred.SetSink(second); deferred.WriteValue("b", 2, OpcUaQuality.Good, DateTime.UtcNow); first.Calls.Single().ShouldBe("WV:a"); second.Calls.Single().ShouldBe("WV:b"); } private sealed class RecordingSink : IOpcUaAddressSpaceSink { /// Gets the queue of recorded calls. public ConcurrentQueue CallQueue { get; } = new(); /// Gets the list of recorded calls. public List Calls => CallQueue.ToList(); /// public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) => CallQueue.Enqueue($"WV:{nodeId}"); /// public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc) => CallQueue.Enqueue($"WA:{alarmNodeId}"); /// public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) => CallQueue.Enqueue($"EF:{folderNodeId}"); /// public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType) => CallQueue.Enqueue($"EV:{variableNodeId}"); /// public void RebuildAddressSpace() => CallQueue.Enqueue("RB"); } }