using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.OpcUaServer; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests; /// /// Gitea #691.4 review: events read uncapped (and walked back newest-first) through the REAL /// over a gateway-shaped fake — inclusive [start, end], ordered by event time. /// public sealed class GatewayReverseEventsTests { private static CancellationToken Ct => TestContext.Current.CancellationToken; private static readonly DateTime T0 = new(2026, 10, 12, 18, 0, 0, DateTimeKind.Utc); private static FakeHistorianGatewayClient Store(int count) { var stored = Enumerable.Range(0, count) .Select(i => new HistorianEvent { Id = $"e{i:0000}", Type = "Alarm", EventTime = Timestamp.FromDateTime(T0.AddSeconds(i)) }) .ToList(); return new FakeHistorianGatewayClient { EventsSource = (start, end) => stored.Where(e => e.EventTime.ToDateTime() >= start && e.EventTime.ToDateTime() <= end), }; } /// /// NumValuesPerNode = 0 reaches the source as int.MaxValue; the result list used to be pre-sized to it /// ("Array dimensions exceeded") — every uncapped Gateway events read failed and counted as a backend failure. /// [Fact] public async Task An_uncapped_forward_events_read_is_served() { var fake = Store(30); var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); var r = await ds.ReadEventsAsync(null, T0, T0.AddMinutes(1), int.MaxValue, Ct); r.Events.Count.ShouldBe(30); r.ContinuationPoint.ShouldBeNull(); ds.GetHealthSnapshot().TotalFailures.ShouldBe(0); } [Fact] public async Task An_uncapped_reverse_events_read_is_one_forward_read_newest_first() { var fake = Store(30); var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); var page = await ReverseEventsReader.ReadAsync(ds, null, hi: T0.AddSeconds(25), lo: T0.AddSeconds(5), want: 0, maxTieCluster: 65536, nowUtc: T0.AddHours(1), hint: default, Ct); page.Events.Select(e => e.EventId).ShouldBe(Enumerable.Range(6, 20).Reverse().Select(i => $"e{i:0000}")); fake.ReadEventsCallCount.ShouldBe(1); ds.GetHealthSnapshot().TotalFailures.ShouldBe(0); } [Fact] public async Task A_capped_reverse_events_read_walked_back_over_the_gateway_returns_the_latest_newest_first() { var fake = Store(300); var ds = new GatewayHistorianDataSource(fake, NullLogger.Instance); var page = await ReverseEventsReader.ReadAsync(ds, null, hi: T0.AddSeconds(200), lo: T0.AddSeconds(50), want: 10, maxTieCluster: 65536, nowUtc: T0.AddHours(1), hint: default, Ct); page.Events.Select(e => e.EventId).ShouldBe(Enumerable.Range(191, 10).Reverse().Select(i => $"e{i:0000}")); fake.LastReadEventsStartUtc.ShouldBeLessThanOrEqualTo(fake.LastReadEventsEndUtc); ds.GetHealthSnapshot().TotalFailures.ShouldBe(0); } }