using System.Runtime.CompilerServices; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; internal class FakeTwinCATClient : ITwinCATClient { /// Gets a value indicating whether the client is connected. 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); /// Simulates connecting to the TwinCAT system. /// The AMS address to connect to. /// The connection timeout. /// The cancellation token. /// A task that completes when the connection succeeds or fails. public virtual Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken ct) { ConnectCount++; if (ThrowOnConnect) throw Exception ?? new InvalidOperationException(); IsConnected = true; return Task.CompletedTask; } /// Simulates reading a value from a symbol. /// The path to the symbol to read. /// The data type of the symbol. /// The optional bit index for bit-level reads. /// The cancellation token. /// A task that returns the simulated value and status. 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)); } /// Simulates writing a value to a symbol. /// The path to the symbol to write. /// The data type of the symbol. /// The optional bit index for bit-level writes. /// The value to write. /// The cancellation token. /// A task that returns the write 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); } /// Simulates probing the connection status. /// The cancellation token. /// A task that returns the probe result. 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; } /// Simulates adding a notification for value changes. /// The path to the symbol to watch. /// The data type of the symbol. /// The optional bit index for bit-level notifications. /// The sampling cycle time. /// The maximum delay in milliseconds. /// The callback to invoke on value change. /// The cancellation token. /// A task that returns a notification handle. 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; } /// Simulates browsing the symbol tree. /// The cancellation token. /// An async enumerable of discovered symbols. 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; } /// Creates a new fake TwinCAT client. /// A newly created client instance. public ITwinCATClient Create() { var client = Customise?.Invoke() ?? new FakeTwinCATClient(); Clients.Add(client); return client; } }