using System.Runtime.CompilerServices;
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
///
/// Reusable in-memory test double for . 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
/// RpcException) per operation; reads share .
///
public sealed class FakeHistorianGatewayClient : IHistorianGatewayClient
{
public bool TagExistsResult = true;
public int TagExistsCallCount;
public Exception? TagExistsThrows;
/// Optional asynchronous catalog response for overlapping presence-cache tests.
public Func>? TagExistsSource;
public Task TagExistsAsync(string tag, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
Interlocked.Increment(ref TagExistsCallCount);
if (TagExistsSource is { } source) return source(tag, ct);
if (TagExistsThrows is { } error) throw error;
return Task.FromResult(TagExistsResult);
}
// ---- ReadRaw -------------------------------------------------------------------------------
public IReadOnlyList RawSamples = Array.Empty();
public string? LastReadRawTag;
public DateTime LastReadRawStartUtc;
public DateTime LastReadRawEndUtc;
public int LastReadRawMaxValues;
public int ReadRawCallCount;
///
/// When set, ReadRaw serves RawSource(start, end, maxValues) instead of (a
/// store-like fake honouring the window, #681).
///
public Func>? RawSource;
/// Raw samples actually pulled by the consumer across every ReadRaw stream.
public int RawYieldedCount;
/// ReadRaw streams the consumer enumerated to the END (never broke out of — #681 review).
public int RawStreamsCompleted;
/// Every ReadRaw call's (start, end, maxValues), in order.
public readonly List<(DateTime Start, DateTime End, int MaxValues)> RawCalls = new();
// ---- ReadAggregate -------------------------------------------------------------------------
public IReadOnlyList AggregateSamples = Array.Empty();
public string? LastAggregateTag;
public DateTime LastAggregateStartUtc;
public DateTime LastAggregateEndUtc;
public RetrievalMode LastAggregateMode;
public TimeSpan LastAggregateInterval;
public int ReadAggregateCallCount;
// ---- ReadAtTime ----------------------------------------------------------------------------
public IReadOnlyList AtTimeSamples = Array.Empty();
public string? LastReadAtTimeTag;
public IReadOnlyList? LastReadAtTimeTimestamps;
public int ReadAtTimeCallCount;
// ---- ReadEvents ----------------------------------------------------------------------------
public IReadOnlyList Events = Array.Empty();
public string? LastReadEventsSourceName;
public DateTime LastReadEventsStartUtc;
public DateTime LastReadEventsEndUtc;
public int LastReadEventsMaxEvents;
public int ReadEventsCallCount;
/// When set, ReadEvents serves EventsSource(start, end) instead of .
public Func>? EventsSource;
/// Thrown (deferred to first enumeration) by every read method when set.
public Exception? ThrowOnRead;
// ---- WriteLiveValues -----------------------------------------------------------------------
public WriteAck WriteLiveValuesResult = new() { Success = true };
public string? LastWriteLiveTag;
public IReadOnlyList? 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? 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 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(),
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));
}
///
/// A strict gateway (Historian:ReadLimits:AllowStreamingBeyondCeiling=false) with this
/// MaxValuesCeiling: a ReadRaw asking for more is refused InvalidArgument (#691.3).
///
public int? MaxValuesCeiling;
/// Overrides the refusal's detail text (an older/other gateway wording).
public string? CeilingRejectionDetail;
public IAsyncEnumerable 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 || interval <= TimeSpan.Zero)
{
// The real gateway's request validation (ValidateRange; interval must be > 0), raised before any row.
var rpc = new Grpc.Core.RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.InvalidArgument,
startUtc > endUtc ? "start_time must be less than or equal to end_time." : "interval must be greater than zero."));
return ToAsyncStream(Array.Empty(),
new ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayException(rpc.Status.Detail, rpc), ct);
}
return ToAsyncStream(AggregateSource(startUtc, endUtc, mode, interval).ToList(), ThrowOnRead, ct);
}
/// When set, ReadAggregate serves AggregateSource(start, end, mode, interval) (and refuses start > end).
public Func>? AggregateSource;
/// Every ReadAggregate call's (start, end, mode, interval), in order.
public readonly List<(DateTime Start, DateTime End, RetrievalMode Mode, TimeSpan Interval)> AggregateCalls = new();
public Task> ReadAtTimeAsync(
string tag,
IReadOnlyList timestampsUtc,
CancellationToken ct)
{
LastReadAtTimeTag = tag;
LastReadAtTimeTimestamps = timestampsUtc;
ReadAtTimeCallCount++;
return ThrowOnRead is not null
? Task.FromException>(ThrowOnRead)
: Task.FromResult(AtTimeSamples);
}
public IAsyncEnumerable ReadEventsAsync(
string? sourceName,
DateTime startUtc,
DateTime endUtc,
int maxEvents,
CancellationToken ct)
{
LastReadEventsSourceName = sourceName;
LastReadEventsStartUtc = startUtc;
LastReadEventsEndUtc = endUtc;
LastReadEventsMaxEvents = maxEvents;
ReadEventsCallCount++;
return ToAsyncStream(EventsSource is null ? Events : EventsSource(startUtc, endUtc).ToList(), ThrowOnRead, ct);
}
public Task WriteLiveValuesAsync(
string tag,
IReadOnlyList values,
CancellationToken ct)
{
LastWriteLiveTag = tag;
LastWriteLiveValues = values;
WriteLiveValuesCallCount++;
return WriteLiveValuesThrows is not null
? Task.FromException(WriteLiveValuesThrows)
: Task.FromResult(WriteLiveValuesResult);
}
public Task SendEventAsync(HistorianEvent evt, CancellationToken ct)
{
LastSendEvent = evt;
SendEventCallCount++;
return SendEventThrows is not null
? Task.FromException(SendEventThrows)
: Task.FromResult(SendEventResult);
}
public Task EnsureTagsAsync(
IReadOnlyList definitions,
CancellationToken ct)
{
LastEnsureDefinitions = definitions;
EnsureTagsCallCount++;
return EnsureTagsThrows is not null
? Task.FromException(EnsureTagsThrows)
: Task.FromResult(EnsureTagsResult);
}
public Task ProbeAsync(CancellationToken ct)
{
ProbeCallCount++;
return ProbeThrows is not null
? Task.FromException(ProbeThrows)
: Task.FromResult(ProbeResult);
}
public Task GetConnectionStatusAsync(CancellationToken ct)
{
GetConnectionStatusCallCount++;
return GetConnectionStatusThrows is not null
? Task.FromException(GetConnectionStatusThrows)
: Task.FromResult(ConnectionStatus);
}
public ValueTask DisposeAsync()
{
DisposeCallCount++;
return ValueTask.CompletedTask;
}
private async IAsyncEnumerable CountingStream(IAsyncEnumerable inner)
{
await foreach (var item in inner.ConfigureAwait(false))
{
RawYieldedCount++;
yield return item;
}
RawStreamsCompleted++;
}
private static async IAsyncEnumerable ToAsyncStream(
IReadOnlyList 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);
}
}