Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayRawDirectionTests.cs
T
Joseph Doherty 410c8d8976 fix(historian): #686 serve reverse/end-only raw reads on forward-only backends (Gateway)
#669 turned an end-only capped HistoryRead-Raw into a reverse read (start > end)
for every backend; the gateway rejects start > end (InvalidArgument), so on the
default Gateway backend every end-only capped and every explicit reverse raw read
failed the node and was counted as a backend health failure.

- IHistorianDataSource.SupportsReverseRaw (default false; SqlServer/QuestDB/Null
  true, Gateway false; guard test pins that every production backend declares it).
- OtOpcUaNodeManager sends a reverse window only to a backend that declares it;
  otherwise ReverseRawReader walks (lo, hi] back in bounded forward segment reads
  (1 min, x4 while sparse; each <= MaxTieClusterOverfetch + 1 rows; a full read is
  re-read narrower, never answered truncated; filtered on sample timestamps, which
  also drops AVEVA's fabricated query-start rows; stops at the Unix epoch). A
  one-timestamp tie cluster over the bound => BadTooManyOperations.
- ServeRawPagedAsync maps HistoryReadLimitExceededException to BadTooManyOperations.
- GatewayHistorianDataSource.ReadRawAsync refuses start > end unsent, without
  recording a health failure.
2026-10-12 14:42:27 -04:00

46 lines
1.9 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
/// <summary>
/// Gitea #686: the gateway's <c>ReadRaw</c> streams ascending only and rejects <c>start &gt; end</c> with
/// <c>InvalidArgument</c>, which used to be recorded as a backend health failure. The source declares it cannot read
/// newest-first (so the server walks back with forward reads), and refuses a reverse window itself without sending
/// anything or counting a failure.
/// </summary>
public sealed class GatewayRawDirectionTests
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
[Fact]
public void The_gateway_source_declares_it_cannot_read_raw_newest_first()
{
IHistorianDataSource ds = new GatewayHistorianDataSource(
new FakeHistorianGatewayClient(), NullLogger<GatewayHistorianDataSource>.Instance);
ds.SupportsReverseRaw.ShouldBeFalse();
}
[Fact]
public async Task A_reverse_raw_window_is_refused_unsent_and_is_not_a_backend_failure()
{
var fake = new FakeHistorianGatewayClient();
IHistorianDataSource ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
var end = new DateTime(2026, 10, 12, 18, 7, 45, DateTimeKind.Utc);
await Should.ThrowAsync<NotSupportedException>(() =>
ds.ReadRawAsync("Gate681", end, new DateTime(0, DateTimeKind.Utc), 3, Ct));
fake.ReadRawCallCount.ShouldBe(0);
var health = ds.GetHealthSnapshot();
health.TotalFailures.ShouldBe(0);
health.ConsecutiveFailures.ShouldBe(0);
// Positive control: a forward read of the same window is sent.
await ds.ReadRawAsync("Gate681", new DateTime(2026, 10, 12, 18, 0, 0, DateTimeKind.Utc), end, 3, Ct);
fake.ReadRawCallCount.ShouldBe(1);
}
}