277 lines
12 KiB
C#
277 lines
12 KiB
C#
using System.Runtime.CompilerServices;
|
|
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
|
|
|
|
/// <summary>
|
|
/// Reusable in-memory test double for <see cref="IHistorianGatewayClient"/>. Every method returns
|
|
/// from a public settable result field and records its call arguments into public fields, so the
|
|
/// later driver tasks (T7/T8/T11/T12/T14) can drive behaviour and assert on what the driver sent
|
|
/// without a live gateway. Throw fields let a test simulate transport faults (e.g. an
|
|
/// <c>RpcException</c>) per operation; reads share <see cref="ThrowOnRead"/>.
|
|
/// </summary>
|
|
public sealed class FakeHistorianGatewayClient : IHistorianGatewayClient
|
|
{
|
|
// ---- ReadRaw -------------------------------------------------------------------------------
|
|
public IReadOnlyList<HistorianSample> RawSamples = Array.Empty<HistorianSample>();
|
|
public string? LastReadRawTag;
|
|
public DateTime LastReadRawStartUtc;
|
|
public DateTime LastReadRawEndUtc;
|
|
public int LastReadRawMaxValues;
|
|
public int ReadRawCallCount;
|
|
/// <summary>
|
|
/// When set, ReadRaw serves <c>RawSource(start, end, maxValues)</c> instead of <see cref="RawSamples"/> (a
|
|
/// store-like fake honouring the window, #681).
|
|
/// </summary>
|
|
public Func<DateTime, DateTime, int, IEnumerable<HistorianSample>>? RawSource;
|
|
/// <summary>Raw samples actually pulled by the consumer across every ReadRaw stream.</summary>
|
|
public int RawYieldedCount;
|
|
/// <summary>ReadRaw streams the consumer enumerated to the END (never broke out of — #681 review).</summary>
|
|
public int RawStreamsCompleted;
|
|
/// <summary>Every ReadRaw call's (start, end, maxValues), in order.</summary>
|
|
public readonly List<(DateTime Start, DateTime End, int MaxValues)> RawCalls = new();
|
|
|
|
// ---- ReadAggregate -------------------------------------------------------------------------
|
|
public IReadOnlyList<HistorianAggregateSample> AggregateSamples = Array.Empty<HistorianAggregateSample>();
|
|
public string? LastAggregateTag;
|
|
public DateTime LastAggregateStartUtc;
|
|
public DateTime LastAggregateEndUtc;
|
|
public RetrievalMode LastAggregateMode;
|
|
public TimeSpan LastAggregateInterval;
|
|
public int ReadAggregateCallCount;
|
|
|
|
// ---- ReadAtTime ----------------------------------------------------------------------------
|
|
public IReadOnlyList<HistorianSample> AtTimeSamples = Array.Empty<HistorianSample>();
|
|
public string? LastReadAtTimeTag;
|
|
public IReadOnlyList<DateTime>? LastReadAtTimeTimestamps;
|
|
public int ReadAtTimeCallCount;
|
|
|
|
// ---- ReadEvents ----------------------------------------------------------------------------
|
|
public IReadOnlyList<HistorianEvent> Events = Array.Empty<HistorianEvent>();
|
|
public string? LastReadEventsSourceName;
|
|
public DateTime LastReadEventsStartUtc;
|
|
public DateTime LastReadEventsEndUtc;
|
|
public int LastReadEventsMaxEvents;
|
|
public int ReadEventsCallCount;
|
|
|
|
/// <summary>Thrown (deferred to first enumeration) by every read method when set.</summary>
|
|
public Exception? ThrowOnRead;
|
|
|
|
// ---- WriteLiveValues -----------------------------------------------------------------------
|
|
public WriteAck WriteLiveValuesResult = new() { Success = true };
|
|
public string? LastWriteLiveTag;
|
|
public IReadOnlyList<HistorianLiveValue>? LastWriteLiveValues;
|
|
public int WriteLiveValuesCallCount;
|
|
public Exception? WriteLiveValuesThrows;
|
|
|
|
// ---- SendEvent -----------------------------------------------------------------------------
|
|
public WriteAck SendEventResult = new() { Success = true };
|
|
public HistorianEvent? LastSendEvent;
|
|
public int SendEventCallCount;
|
|
public Exception? SendEventThrows;
|
|
|
|
// ---- EnsureTags ----------------------------------------------------------------------------
|
|
public TagOperationResults EnsureTagsResult = new();
|
|
public IReadOnlyList<HistorianTagDefinition>? LastEnsureDefinitions;
|
|
public int EnsureTagsCallCount;
|
|
public Exception? EnsureTagsThrows;
|
|
|
|
// ---- Probe ---------------------------------------------------------------------------------
|
|
public bool ProbeResult = true;
|
|
public int ProbeCallCount;
|
|
public Exception? ProbeThrows;
|
|
|
|
// ---- GetConnectionStatus -------------------------------------------------------------------
|
|
public ConnectionStatus ConnectionStatus = new();
|
|
public int GetConnectionStatusCallCount;
|
|
public Exception? GetConnectionStatusThrows;
|
|
|
|
// ---- Dispose -------------------------------------------------------------------------------
|
|
public int DisposeCallCount;
|
|
|
|
public IAsyncEnumerable<HistorianSample> ReadRawAsync(
|
|
string tag,
|
|
DateTime startUtc,
|
|
DateTime endUtc,
|
|
int maxValues,
|
|
CancellationToken ct)
|
|
{
|
|
LastReadRawTag = tag;
|
|
LastReadRawStartUtc = startUtc;
|
|
LastReadRawEndUtc = endUtc;
|
|
LastReadRawMaxValues = maxValues;
|
|
lock (RawCalls)
|
|
{
|
|
ReadRawCallCount++;
|
|
RawCalls.Add((startUtc, endUtc, maxValues));
|
|
}
|
|
if (MaxValuesCeiling is { } ceiling && maxValues > ceiling)
|
|
{
|
|
// The real gateway's refusal (HistorianReadService.ReadRaw), raised on the first MoveNext before any row, as
|
|
// the client package surfaces it: a HistorianGatewayException wrapping the RpcException.
|
|
var rpc = new Grpc.Core.RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.InvalidArgument,
|
|
CeilingRejectionDetail ?? $"max_values exceeds the server ceiling of {ceiling}; read large ranges by time-slicing " +
|
|
"(split the range and read the slices sequentially), or enable " +
|
|
"Historian:ReadLimits:AllowStreamingBeyondCeiling to page through server-side."));
|
|
return ToAsyncStream(Array.Empty<HistorianSample>(),
|
|
new ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayException(rpc.Status.Detail, rpc), ct);
|
|
}
|
|
var items = RawSource is null ? RawSamples : RawSource(startUtc, endUtc, maxValues).ToList();
|
|
return CountingStream(ToAsyncStream(items, ThrowOnRead, ct));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A strict gateway (<c>Historian:ReadLimits:AllowStreamingBeyondCeiling=false</c>) with this
|
|
/// <c>MaxValuesCeiling</c>: a ReadRaw asking for more is refused <c>InvalidArgument</c> (#691.3).
|
|
/// </summary>
|
|
public int? MaxValuesCeiling;
|
|
|
|
/// <summary>Overrides the refusal's detail text (an older/other gateway wording).</summary>
|
|
public string? CeilingRejectionDetail;
|
|
|
|
public IAsyncEnumerable<HistorianAggregateSample> ReadAggregateAsync(
|
|
string tag,
|
|
DateTime startUtc,
|
|
DateTime endUtc,
|
|
RetrievalMode mode,
|
|
TimeSpan interval,
|
|
CancellationToken ct)
|
|
{
|
|
LastAggregateTag = tag;
|
|
LastAggregateStartUtc = startUtc;
|
|
LastAggregateEndUtc = endUtc;
|
|
LastAggregateMode = mode;
|
|
LastAggregateInterval = interval;
|
|
ReadAggregateCallCount++;
|
|
AggregateCalls.Add((startUtc, endUtc, mode, interval));
|
|
if (AggregateSource is null) return ToAsyncStream(AggregateSamples, ThrowOnRead, ct);
|
|
if (startUtc > endUtc)
|
|
{
|
|
// The real gateway's ValidateRange refusal, raised before any row.
|
|
var rpc = new Grpc.Core.RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.InvalidArgument,
|
|
"start_time must be less than or equal to end_time."));
|
|
return ToAsyncStream(Array.Empty<HistorianAggregateSample>(),
|
|
new ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayException(rpc.Status.Detail, rpc), ct);
|
|
}
|
|
return ToAsyncStream(AggregateSource(startUtc, endUtc, mode, interval).ToList(), ThrowOnRead, ct);
|
|
}
|
|
|
|
/// <summary>When set, ReadAggregate serves <c>AggregateSource(start, end, mode, interval)</c> (and refuses start > end).</summary>
|
|
public Func<DateTime, DateTime, RetrievalMode, TimeSpan, IEnumerable<HistorianAggregateSample>>? AggregateSource;
|
|
|
|
/// <summary>Every ReadAggregate call's (start, end, mode, interval), in order.</summary>
|
|
public readonly List<(DateTime Start, DateTime End, RetrievalMode Mode, TimeSpan Interval)> AggregateCalls = new();
|
|
|
|
public Task<IReadOnlyList<HistorianSample>> ReadAtTimeAsync(
|
|
string tag,
|
|
IReadOnlyList<DateTime> timestampsUtc,
|
|
CancellationToken ct)
|
|
{
|
|
LastReadAtTimeTag = tag;
|
|
LastReadAtTimeTimestamps = timestampsUtc;
|
|
ReadAtTimeCallCount++;
|
|
return ThrowOnRead is not null
|
|
? Task.FromException<IReadOnlyList<HistorianSample>>(ThrowOnRead)
|
|
: Task.FromResult(AtTimeSamples);
|
|
}
|
|
|
|
public IAsyncEnumerable<HistorianEvent> ReadEventsAsync(
|
|
string? sourceName,
|
|
DateTime startUtc,
|
|
DateTime endUtc,
|
|
int maxEvents,
|
|
CancellationToken ct)
|
|
{
|
|
LastReadEventsSourceName = sourceName;
|
|
LastReadEventsStartUtc = startUtc;
|
|
LastReadEventsEndUtc = endUtc;
|
|
LastReadEventsMaxEvents = maxEvents;
|
|
ReadEventsCallCount++;
|
|
return ToAsyncStream(Events, ThrowOnRead, ct);
|
|
}
|
|
|
|
public Task<WriteAck> WriteLiveValuesAsync(
|
|
string tag,
|
|
IReadOnlyList<HistorianLiveValue> values,
|
|
CancellationToken ct)
|
|
{
|
|
LastWriteLiveTag = tag;
|
|
LastWriteLiveValues = values;
|
|
WriteLiveValuesCallCount++;
|
|
return WriteLiveValuesThrows is not null
|
|
? Task.FromException<WriteAck>(WriteLiveValuesThrows)
|
|
: Task.FromResult(WriteLiveValuesResult);
|
|
}
|
|
|
|
public Task<WriteAck> SendEventAsync(HistorianEvent evt, CancellationToken ct)
|
|
{
|
|
LastSendEvent = evt;
|
|
SendEventCallCount++;
|
|
return SendEventThrows is not null
|
|
? Task.FromException<WriteAck>(SendEventThrows)
|
|
: Task.FromResult(SendEventResult);
|
|
}
|
|
|
|
public Task<TagOperationResults> EnsureTagsAsync(
|
|
IReadOnlyList<HistorianTagDefinition> definitions,
|
|
CancellationToken ct)
|
|
{
|
|
LastEnsureDefinitions = definitions;
|
|
EnsureTagsCallCount++;
|
|
return EnsureTagsThrows is not null
|
|
? Task.FromException<TagOperationResults>(EnsureTagsThrows)
|
|
: Task.FromResult(EnsureTagsResult);
|
|
}
|
|
|
|
public Task<bool> ProbeAsync(CancellationToken ct)
|
|
{
|
|
ProbeCallCount++;
|
|
return ProbeThrows is not null
|
|
? Task.FromException<bool>(ProbeThrows)
|
|
: Task.FromResult(ProbeResult);
|
|
}
|
|
|
|
public Task<ConnectionStatus> GetConnectionStatusAsync(CancellationToken ct)
|
|
{
|
|
GetConnectionStatusCallCount++;
|
|
return GetConnectionStatusThrows is not null
|
|
? Task.FromException<ConnectionStatus>(GetConnectionStatusThrows)
|
|
: Task.FromResult(ConnectionStatus);
|
|
}
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
DisposeCallCount++;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
private async IAsyncEnumerable<HistorianSample> CountingStream(IAsyncEnumerable<HistorianSample> inner)
|
|
{
|
|
await foreach (var item in inner.ConfigureAwait(false))
|
|
{
|
|
RawYieldedCount++;
|
|
yield return item;
|
|
}
|
|
RawStreamsCompleted++;
|
|
}
|
|
|
|
private static async IAsyncEnumerable<T> ToAsyncStream<T>(
|
|
IReadOnlyList<T> items,
|
|
Exception? error,
|
|
[EnumeratorCancellation] CancellationToken ct)
|
|
{
|
|
if (error is not null)
|
|
{
|
|
throw error;
|
|
}
|
|
|
|
foreach (var item in items)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
yield return item;
|
|
}
|
|
|
|
await ValueTask.CompletedTask.ConfigureAwait(false);
|
|
}
|
|
}
|