using System.Runtime.CompilerServices; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; internal class FakeTwinCATClient : ITwinCATClient { /// public bool IsConnected { get; private set; } /// Gets the number of times Connect has been called. public int ConnectCount { get; private set; } /// Gets the number of times Dispose has been called. public int DisposeCount { get; private set; } /// Gets or sets a value indicating whether ConnectAsync should throw. public bool ThrowOnConnect { get; set; } /// Gets or sets a value indicating whether ReadValueAsync should throw. public bool ThrowOnRead { get; set; } /// Gets or sets a value indicating whether WriteValueAsync should throw. public bool ThrowOnWrite { get; set; } /// Gets or sets a value indicating whether ProbeAsync should throw. public bool ThrowOnProbe { get; set; } /// Gets or sets the exception to throw when a throw flag is set. public Exception? Exception { get; set; } /// Gets the simulated values by symbol path. public Dictionary Values { get; } = new(StringComparer.OrdinalIgnoreCase); /// Gets the read statuses by symbol path. public Dictionary ReadStatuses { get; } = new(StringComparer.OrdinalIgnoreCase); /// Gets the write statuses by symbol path. public Dictionary WriteStatuses { get; } = new(StringComparer.OrdinalIgnoreCase); /// Gets the log of all write operations. public List<(string symbol, TwinCATDataType type, int? bit, object? value)> WriteLog { get; } = new(); /// Gets or sets the result returned by ProbeAsync. public bool ProbeResult { get; set; } = true; /// Occurs when the symbol version changes. public event EventHandler? OnSymbolVersionChanged; /// Test hook — fire the symbol-version-changed signal as the real client would. public void FireSymbolVersionChanged() => OnSymbolVersionChanged?.Invoke(this, EventArgs.Empty); /// public virtual Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken ct) { ConnectCount++; if (ThrowOnConnect) throw Exception ?? new InvalidOperationException(); IsConnected = true; return Task.CompletedTask; } /// public virtual Task<(object? value, uint status)> ReadValueAsync( string symbolPath, TwinCATDataType type, int? bitIndex, CancellationToken ct) { if (ThrowOnRead) throw Exception ?? new InvalidOperationException(); var status = ReadStatuses.TryGetValue(symbolPath, out var s) ? s : TwinCATStatusMapper.Good; var value = Values.TryGetValue(symbolPath, out var v) ? v : null; return Task.FromResult((value, status)); } /// public virtual Task WriteValueAsync( string symbolPath, TwinCATDataType type, int? bitIndex, object? value, CancellationToken ct) { if (ThrowOnWrite) throw Exception ?? new InvalidOperationException(); WriteLog.Add((symbolPath, type, bitIndex, value)); Values[symbolPath] = value; var status = WriteStatuses.TryGetValue(symbolPath, out var s) ? s : TwinCATStatusMapper.Good; return Task.FromResult(status); } /// public virtual Task ProbeAsync(CancellationToken ct) { if (ThrowOnProbe) return Task.FromResult(false); return Task.FromResult(ProbeResult); } /// Releases unmanaged resources. public virtual void Dispose() { DisposeCount++; IsConnected = false; } // ---- notification fake ---- /// Gets the list of registered notifications. public List Notifications { get; } = new(); /// Gets or sets a value indicating whether AddNotificationAsync should throw. public bool ThrowOnAddNotification { get; set; } /// Records the most recently-supplied maxDelayMs for Driver.TwinCAT-014 tests. public int LastMaxDelayMs { get; private set; } /// public virtual Task AddNotificationAsync( string symbolPath, TwinCATDataType type, int? bitIndex, TimeSpan cycleTime, int maxDelayMs, Action onChange, CancellationToken cancellationToken) { if (ThrowOnAddNotification) throw Exception ?? new InvalidOperationException("fake AddNotification failure"); LastMaxDelayMs = maxDelayMs; var reg = new FakeNotification(symbolPath, type, bitIndex, onChange, this); Notifications.Add(reg); return Task.FromResult(reg); } /// Fire a change event through the registered callback for . /// The symbol path for which to fire the change. /// The new value to pass to the callback. public void FireNotification(string symbolPath, object? value) { foreach (var n in Notifications) if (!n.Disposed && string.Equals(n.SymbolPath, symbolPath, StringComparison.OrdinalIgnoreCase)) n.OnChange(symbolPath, value); } // ---- symbol browser fake ---- /// Gets the simulated browse results. public List BrowseResults { get; } = new(); /// Gets or sets a value indicating whether BrowseSymbolsAsync should throw. public bool ThrowOnBrowse { get; set; } /// public virtual async IAsyncEnumerable BrowseSymbolsAsync( [EnumeratorCancellation] CancellationToken cancellationToken) { if (ThrowOnBrowse) throw Exception ?? new InvalidOperationException("fake browse failure"); await Task.CompletedTask; foreach (var sym in BrowseResults) { if (cancellationToken.IsCancellationRequested) yield break; yield return sym; } } /// Represents a registered notification in the fake client. public sealed class FakeNotification( string symbolPath, TwinCATDataType type, int? bitIndex, Action onChange, FakeTwinCATClient owner) : ITwinCATNotificationHandle { /// Gets the symbol path being watched. public string SymbolPath { get; } = symbolPath; /// Gets the data type of the symbol. public TwinCATDataType Type { get; } = type; /// Gets the optional bit index. public int? BitIndex { get; } = bitIndex; /// Gets the callback to invoke on value change. public Action OnChange { get; } = onChange; /// Gets a value indicating whether this notification has been disposed. public bool Disposed { get; private set; } /// Disposes this notification handle. public void Dispose() { Disposed = true; owner.Notifications.Remove(this); } } } /// Represents a factory for creating fake TwinCAT clients. internal sealed class FakeTwinCATClientFactory : ITwinCATClientFactory { /// Gets the list of clients created by this factory. public List Clients { get; } = new(); /// Gets or sets an optional customization function for creating clients. public Func? Customise { get; set; } /// public ITwinCATClient Create() { var client = Customise?.Invoke() ?? new FakeTwinCATClient(); Clients.Add(client); return client; } }