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");
}
}