using System.Collections.Concurrent; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests; /// Test fake implementation of for verifying subscription behavior. public sealed class FakeUpstream : ITagUpstreamSource { private readonly ConcurrentDictionary _values = new(StringComparer.Ordinal); private readonly ConcurrentDictionary>> _subs = new(StringComparer.Ordinal); /// Gets the current count of active subscriptions. public int ActiveSubscriptionCount { get; private set; } /// Sets a tag value without notifying subscribers. /// The tag path to set. /// The value to set for the tag. /// The OPC UA status code for the value. public void Set(string path, object? value, uint statusCode = 0u) { var now = DateTime.UtcNow; _values[path] = new DataValueSnapshot(value, statusCode, now, now); } /// Sets a tag value and notifies all current subscribers. /// The tag path to set. /// The value to set for the tag. /// The OPC UA status code for the value. public void Push(string path, object? value, uint statusCode = 0u) { Set(path, value, statusCode); if (_subs.TryGetValue(path, out var list)) { Action[] snap; lock (list) { snap = list.ToArray(); } foreach (var obs in snap) obs(path, _values[path]); } } /// Reads the current value of a tag, or returns a bad-status snapshot if not set. /// The tag path to read. public DataValueSnapshot ReadTag(string path) => _values.TryGetValue(path, out var v) ? v : new DataValueSnapshot(null, 0x80340000u, null, DateTime.UtcNow); /// Subscribes an observer to tag changes for the given path. /// The tag path to subscribe to. /// The observer callback to invoke on tag changes. public IDisposable SubscribeTag(string path, Action observer) { var list = _subs.GetOrAdd(path, _ => []); lock (list) { list.Add(observer); } ActiveSubscriptionCount++; return new Unsub(this, path, observer); } /// Disposable subscription handle that unsubscribes the observer when disposed. private sealed class Unsub : IDisposable { private readonly FakeUpstream _up; private readonly string _path; private readonly Action _observer; /// Initializes the unsubscription handle with references needed to clean up the subscription. /// The upstream source containing the subscription list. /// The tag path to unsubscribe from. /// The observer to remove from the subscription list. public Unsub(FakeUpstream up, string path, Action observer) { _up = up; _path = path; _observer = observer; } /// Removes the observer from the subscription list. public void Dispose() { if (_up._subs.TryGetValue(_path, out var list)) { lock (list) { if (list.Remove(_observer)) _up.ActiveSubscriptionCount--; } } } } }