Files
lmxopcua/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/Fakes/FakeOpcUaClientService.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

286 lines
12 KiB
C#

using Opc.Ua;
using ZB.MOM.WW.OtOpcUa.Client.Shared;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;
namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
/// <summary>
/// Fake implementation of <see cref="IOpcUaClientService" /> for unit testing commands.
/// Records all method calls and returns configurable results.
/// </summary>
public sealed class FakeOpcUaClientService : IOpcUaClientService
{
// Track calls
/// <summary>Gets a value indicating whether ConnectAsync was called.</summary>
public bool ConnectCalled { get; private set; }
/// <summary>Gets the connection settings from the most recent ConnectAsync call.</summary>
public ConnectionSettings? LastConnectionSettings { get; private set; }
/// <summary>Gets a value indicating whether DisconnectAsync was called.</summary>
public bool DisconnectCalled { get; private set; }
/// <summary>Gets a value indicating whether Dispose was called.</summary>
public bool DisposeCalled { get; private set; }
/// <summary>Gets the list of node IDs passed to ReadValueAsync calls.</summary>
public List<NodeId> ReadNodeIds { get; } = [];
/// <summary>Gets the list of (NodeId, value) pairs passed to WriteValueAsync calls.</summary>
public List<(NodeId NodeId, object Value)> WriteValues { get; } = [];
/// <summary>Gets the list of parent node IDs passed to BrowseAsync calls.</summary>
public List<NodeId?> BrowseNodeIds { get; } = [];
/// <summary>Gets the list of (NodeId, intervalMs) pairs from SubscribeAsync calls.</summary>
public List<(NodeId NodeId, int IntervalMs)> SubscribeCalls { get; } = [];
/// <summary>Gets the list of node IDs passed to UnsubscribeAsync calls.</summary>
public List<NodeId> UnsubscribeCalls { get; } = [];
/// <summary>Gets the list of (SourceNodeId, intervalMs) pairs from SubscribeAlarmsAsync calls.</summary>
public List<(NodeId? SourceNodeId, int IntervalMs)> SubscribeAlarmsCalls { get; } = [];
/// <summary>Gets a value indicating whether UnsubscribeAlarmsAsync was called.</summary>
public bool UnsubscribeAlarmsCalled { get; private set; }
/// <summary>Gets a value indicating whether RequestConditionRefreshAsync was called.</summary>
public bool RequestConditionRefreshCalled { get; private set; }
/// <summary>Gets the list of (NodeId, start, end, maxValues) tuples from HistoryReadRawAsync calls.</summary>
public List<(NodeId NodeId, DateTime Start, DateTime End, int MaxValues)> HistoryReadRawCalls { get; } = [];
/// <summary>Gets the list of history read aggregate call parameters.</summary>
public List<(NodeId NodeId, DateTime Start, DateTime End, AggregateType Aggregate, double IntervalMs)>
HistoryReadAggregateCalls { get; } =
[];
/// <summary>Gets a value indicating whether GetRedundancyInfoAsync was called.</summary>
public bool GetRedundancyInfoCalled { get; private set; }
// Configurable results
/// <summary>Gets or sets the connection info returned by ConnectAsync.</summary>
public ConnectionInfo ConnectionInfoResult { get; set; } = new(
"opc.tcp://localhost:4840",
"TestServer",
"None",
"http://opcfoundation.org/UA/SecurityPolicy#None",
"session-1",
"TestSession");
/// <summary>Gets or sets the data value returned by ReadValueAsync.</summary>
public DataValue ReadValueResult { get; set; } = new(
new Variant(42),
StatusCodes.Good,
DateTime.UtcNow,
DateTime.UtcNow);
/// <summary>Gets or sets the status code returned by WriteValueAsync.</summary>
public StatusCode WriteStatusCodeResult { get; set; } = StatusCodes.Good;
/// <summary>Gets or sets the browse results returned by BrowseAsync.</summary>
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = new List<BrowseResult>
{
new("ns=2;s=Node1", "Node1", "Object", true),
new("ns=2;s=Node2", "Node2", "Variable", false)
};
/// <summary>
/// Optional per-parent-node browse results. When a key matches the requested parent's
/// <see cref="NodeId.ToString" />, this dictionary takes precedence over <see cref="BrowseResults" />.
/// Tests exercising recursive walks (Client.CLI-010) use it to model a real subtree whose
/// child node ids do not collide on descent.
/// </summary>
public Dictionary<string, IReadOnlyList<BrowseResult>> BrowseResultsByParent { get; } = new();
/// <summary>Gets or sets the history read result returned by HistoryReadRawAsync and HistoryReadAggregateAsync.</summary>
public IReadOnlyList<DataValue> HistoryReadResult { get; set; } = new List<DataValue>
{
new(new Variant(10.0), StatusCodes.Good, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow),
new(new Variant(20.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
};
/// <summary>Gets or sets the redundancy info returned by GetRedundancyInfoAsync.</summary>
public RedundancyInfo RedundancyInfoResult { get; set; } = new(
"Warm", 200, ["urn:server1", "urn:server2"], "urn:app:test");
/// <summary>Gets or sets the exception thrown by ConnectAsync.</summary>
public Exception? ConnectException { get; set; }
/// <summary>Gets or sets the exception thrown by ReadValueAsync.</summary>
public Exception? ReadException { get; set; }
/// <summary>Gets or sets the exception thrown by WriteValueAsync.</summary>
public Exception? WriteException { get; set; }
/// <summary>Gets or sets the exception thrown by RequestConditionRefreshAsync.</summary>
public Exception? ConditionRefreshException { get; set; }
/// <summary>Gets or sets the exception thrown by SubscribeAsync.</summary>
public Exception? SubscribeException { get; set; }
/// <inheritdoc />
public bool IsConnected => ConnectCalled && !DisconnectCalled;
/// <inheritdoc />
public ConnectionInfo? CurrentConnectionInfo => ConnectCalled ? ConnectionInfoResult : null;
/// <inheritdoc />
public event EventHandler<DataChangedEventArgs>? DataChanged;
/// <inheritdoc />
public event EventHandler<AlarmEventArgs>? AlarmEvent;
/// <inheritdoc />
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
/// <summary>True when at least one handler is attached to <see cref="DataChanged" />.</summary>
public bool HasDataChangedSubscribers => DataChanged != null;
/// <summary>True when at least one handler is attached to <see cref="AlarmEvent" />.</summary>
public bool HasAlarmEventSubscribers => AlarmEvent != null;
/// <inheritdoc />
public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
{
ConnectCalled = true;
LastConnectionSettings = settings;
if (ConnectException != null) throw ConnectException;
return Task.FromResult(ConnectionInfoResult);
}
/// <inheritdoc />
public Task DisconnectAsync(CancellationToken ct = default)
{
DisconnectCalled = true;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default)
{
ReadNodeIds.Add(nodeId);
if (ReadException != null) throw ReadException;
return Task.FromResult(ReadValueResult);
}
/// <inheritdoc />
public Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default)
{
WriteValues.Add((nodeId, value));
if (WriteException != null) throw WriteException;
return Task.FromResult(WriteStatusCodeResult);
}
/// <inheritdoc />
public Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default)
{
BrowseNodeIds.Add(parentNodeId);
var key = parentNodeId?.ToString();
if (key != null && BrowseResultsByParent.TryGetValue(key, out var keyed))
return Task.FromResult(keyed);
return Task.FromResult(BrowseResults);
}
/// <inheritdoc />
public Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default)
{
SubscribeCalls.Add((nodeId, intervalMs));
if (SubscribeException != null) throw SubscribeException;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default)
{
UnsubscribeCalls.Add(nodeId);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
{
SubscribeAlarmsCalls.Add((sourceNodeId, intervalMs));
return Task.CompletedTask;
}
/// <inheritdoc />
public Task UnsubscribeAlarmsAsync(CancellationToken ct = default)
{
UnsubscribeAlarmsCalled = true;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task RequestConditionRefreshAsync(CancellationToken ct = default)
{
RequestConditionRefreshCalled = true;
if (ConditionRefreshException != null) throw ConditionRefreshException;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
CancellationToken ct = default)
{
return Task.FromResult(new StatusCode(StatusCodes.Good));
}
/// <inheritdoc />
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
{
HistoryReadRawCalls.Add((nodeId, startTime, endTime, maxValues));
return Task.FromResult(HistoryReadResult);
}
/// <inheritdoc />
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate,
double intervalMs = 3600000, CancellationToken ct = default)
{
HistoryReadAggregateCalls.Add((nodeId, startTime, endTime, aggregate, intervalMs));
return Task.FromResult(HistoryReadResult);
}
/// <inheritdoc />
public Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default)
{
GetRedundancyInfoCalled = true;
return Task.FromResult(RedundancyInfoResult);
}
/// <summary>
/// Marks the fake client as disposed so CLI command tests can assert cleanup behavior.
/// </summary>
public void Dispose()
{
DisposeCalled = true;
}
/// <summary>Raises the DataChanged event for testing subscribe commands.</summary>
/// <param name="nodeId">The node ID string that changed.</param>
/// <param name="value">The new data value.</param>
public void RaiseDataChanged(string nodeId, DataValue value)
{
DataChanged?.Invoke(this, new DataChangedEventArgs(nodeId, value));
}
/// <summary>Raises the AlarmEvent for testing alarm commands.</summary>
/// <param name="args">The alarm event arguments.</param>
public void RaiseAlarmEvent(AlarmEventArgs args)
{
AlarmEvent?.Invoke(this, args);
}
/// <summary>Raises the ConnectionStateChanged event for testing.</summary>
/// <param name="oldState">The previous connection state.</param>
/// <param name="newState">The new connection state.</param>
/// <param name="endpointUrl">The endpoint URL.</param>
public void RaiseConnectionStateChanged(ConnectionState oldState, ConnectionState newState, string endpointUrl)
{
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(oldState, newState, endpointUrl));
}
}