513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
167 lines
7.5 KiB
C#
167 lines
7.5 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.LoadHarness;
|
|
|
|
/// <summary>
|
|
/// In-process stand-in for an OPC UA / MxGateway server, registered on the REAL
|
|
/// <c>DataConnectionFactory</c> under the <see cref="ProtocolName"/> protocol so the
|
|
/// whole Data Connection Layer above it — <c>DataConnectionManagerActor</c>,
|
|
/// <c>DataConnectionActor</c>, its <c>_instancesByTag</c> fan-out and the
|
|
/// <c>TagValueUpdate</c> hand-off to Instance Actors — runs unmodified.
|
|
///
|
|
/// <para>
|
|
/// It implements <see cref="IBatchSubscribableConnection"/> because the batch path is
|
|
/// the one a real site takes at this scale; the per-tag fallback would make
|
|
/// subscribe setup, not steady-state throughput, the thing being measured.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Why faking here does not invalidate the measurement.</b> Everything this class
|
|
/// replaces is on the far side of the process boundary: socket I/O, the OPC UA SDK's
|
|
/// own session/subscription machinery, and the device. The system under test — the
|
|
/// actor hierarchy, the site stream, store-and-forward, health, and the streaming
|
|
/// relay — begins at the <see cref="SubscriptionCallback"/> invocation, which is
|
|
/// exactly where the real adapter hands off. Driving 375,000 genuine monitored items
|
|
/// would measure the OPC UA stack, not ScadaBridge.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class SimulatedDataConnection : IDataConnection, IBatchSubscribableConnection
|
|
{
|
|
/// <summary>Protocol discriminator this adapter registers under on the factory.</summary>
|
|
public const string ProtocolName = "LoadSim";
|
|
|
|
private SubscriptionCallback? _callback;
|
|
private int _subscriptionCounter;
|
|
|
|
/// <summary>
|
|
/// The data connection name this adapter was created for, taken from the connection
|
|
/// details. The DCL factory creates adapters as its manager actor processes the
|
|
/// CreateConnectionCommands, so creation order is not connection order — the driver
|
|
/// resolves an adapter by name rather than by index.
|
|
/// </summary>
|
|
public string ConnectionName { get; private set; } = string.Empty;
|
|
|
|
/// <summary>Key under which the connection name travels in the connection details.</summary>
|
|
public const string ConnectionNameKey = "connectionName";
|
|
|
|
// Gets the current health of the simulated connection.
|
|
/// <inheritdoc />
|
|
public ConnectionHealth Status { get; private set; } = ConnectionHealth.Disconnected;
|
|
|
|
/// <summary>Raised when the simulated connection is disconnected.</summary>
|
|
public event Action? Disconnected;
|
|
|
|
/// <summary>
|
|
/// The callback captured at subscribe time. The tag driver invokes this to inject
|
|
/// a value change, mirroring what the OPC UA SDK's notification thread does.
|
|
/// Null until the site's Instance Actors have subscribed.
|
|
/// </summary>
|
|
public SubscriptionCallback? ValueCallback => _callback;
|
|
|
|
/// <summary>Number of tag paths this connection has accepted subscriptions for.</summary>
|
|
public int SubscribedTagCount => _subscriptionCounter;
|
|
|
|
// Marks the connection as connected and records the connection name from the details, if present.
|
|
/// <inheritdoc />
|
|
public Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default)
|
|
{
|
|
if (connectionDetails.TryGetValue(ConnectionNameKey, out var name))
|
|
ConnectionName = name;
|
|
|
|
Status = ConnectionHealth.Connected;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// Marks the connection as disconnected and raises the Disconnected event.
|
|
/// <inheritdoc />
|
|
public Task DisconnectAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
Status = ConnectionHealth.Disconnected;
|
|
Disconnected?.Invoke();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// Captures the callback and returns a new subscription id.
|
|
/// <inheritdoc />
|
|
public Task<string> SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default)
|
|
{
|
|
_callback = callback;
|
|
return Task.FromResult($"sub-{Interlocked.Increment(ref _subscriptionCounter)}");
|
|
}
|
|
|
|
// Captures the callback and accepts every tag path in the batch.
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<TagSubscribeResult>> SubscribeBatchAsync(
|
|
IReadOnlyList<string> tagPaths,
|
|
SubscriptionCallback callback,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
_callback = callback;
|
|
var results = new List<TagSubscribeResult>(tagPaths.Count);
|
|
foreach (var path in tagPaths)
|
|
{
|
|
results.Add(new TagSubscribeResult(
|
|
path, true, $"sub-{Interlocked.Increment(ref _subscriptionCounter)}", null));
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<TagSubscribeResult>>(results);
|
|
}
|
|
|
|
// Does nothing; the simulated connection keeps no per-subscription state.
|
|
/// <inheritdoc />
|
|
public Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
|
|
// Does nothing; the simulated connection keeps no per-subscription state.
|
|
/// <inheritdoc />
|
|
public Task UnsubscribeBatchAsync(IReadOnlyList<string> subscriptionIds, CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
|
|
// Returns a good-quality zero value for any tag.
|
|
/// <inheritdoc />
|
|
public Task<ReadResult> ReadAsync(string tagPath, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new ReadResult(true, new TagValue(0d, QualityCode.Good, DateTimeOffset.UtcNow), null));
|
|
|
|
// Returns a good-quality zero value for every requested tag.
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyDictionary<string, ReadResult>> ReadBatchAsync(
|
|
IEnumerable<string> tagPaths, CancellationToken cancellationToken = default)
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
var results = new Dictionary<string, ReadResult>();
|
|
foreach (var path in tagPaths)
|
|
results[path] = new ReadResult(true, new TagValue(0d, QualityCode.Good, now), null);
|
|
|
|
return Task.FromResult<IReadOnlyDictionary<string, ReadResult>>(results);
|
|
}
|
|
|
|
// Accepts the write without storing it.
|
|
/// <inheritdoc />
|
|
public Task<WriteResult> WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new WriteResult(true, null));
|
|
|
|
// Accepts every write in the batch without storing it.
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyDictionary<string, WriteResult>> WriteBatchAsync(
|
|
IDictionary<string, object?> values, CancellationToken cancellationToken = default)
|
|
{
|
|
var results = new Dictionary<string, WriteResult>();
|
|
foreach (var key in values.Keys)
|
|
results[key] = new WriteResult(true, null);
|
|
|
|
return Task.FromResult<IReadOnlyDictionary<string, WriteResult>>(results);
|
|
}
|
|
|
|
// Accepts the batch and reports that the awaited response matched immediately.
|
|
/// <inheritdoc />
|
|
public Task<bool> WriteBatchAndWaitAsync(
|
|
IDictionary<string, object?> values, string flagPath, object? flagValue,
|
|
string responsePath, object? responseValue, TimeSpan timeout,
|
|
CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(true);
|
|
|
|
/// <summary>Releases the connection; the simulated connection holds no resources.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
}
|