3c3513ea3e
Item 1: IHistorianDataSource.MaxRawValuesPerRead (Gateway: learned ceiling - 16 = L; 0 elsewhere, guard-tested). RawPageLimit plans every raw page on top of #692's lookahead: with L > 0 the read (page P + boundary ties k + lookahead) is <= L, P = min(N, L - k - 1) (uncapped: L - k - 1); no room => straight to the tie-cluster over-fetch (never an uncapped or underflowing request). One re-plan + re-read when the read taught a new limit or was refused for crowding pre-window rows (HistoryReadLimitExceededException.PreWindowRows): the page shrinks by them. Over-fetch bound min(MaxTieClusterOverfetch, L - 1); slice size never 0. Review fixes: F1 a window starting at/before the epoch counts AVEVA's synthetic row as pre-window (a ceiling-truncated read was reported complete); F2 the page path disables ReverseRawReader's own re-run (cold end-only ceiling 20/N=3000 cost 3,023 reads, now <= 40); F3 crowded look-back re-read around, not refused; F4 the #687 crowding re-read room at least doubles (up to 8 re-reads), so a cluster of hundreds just before a resume point no longer fails on a default gateway; a crowding refusal that leaves no room is BadTooManyOperations, never a tick-by-tick cursor. Item 2: ServerHistorian:EventReadHorizon (default 00:00:00 = exact, unchanged). When set, every fresh HistoryReadEvents window's earlier bound is raised to now - horizon (both directions, all backends). Lossy by design; documented. Item 3 (native TimeAverage/Total re-check) stays blocked on historiangw#27.
64 lines
2.5 KiB
C#
64 lines
2.5 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
/// <summary>Gitea #694: <see cref="EventHorizon.TryClamp"/> — the pure window arithmetic of <c>ServerHistorian:EventReadHorizon</c>.</summary>
|
|
public sealed class EventHorizonTests
|
|
{
|
|
private static readonly DateTime Now = new(2026, 10, 13, 12, 0, 0, DateTimeKind.Utc);
|
|
private static readonly TimeSpan H = TimeSpan.FromDays(30);
|
|
private static readonly DateTime Floor = Now - H;
|
|
|
|
[Fact]
|
|
public void No_horizon_changes_nothing()
|
|
{
|
|
var (s, e) = (DateTime.MinValue, Now);
|
|
EventHorizon.TryClamp(ref s, ref e, reverse: false, TimeSpan.Zero, Now).ShouldBeTrue();
|
|
(s, e).ShouldBe((DateTime.MinValue, Now));
|
|
EventHorizon.TryClamp(ref s, ref e, reverse: false, TimeSpan.FromDays(-1), Now).ShouldBeTrue();
|
|
(s, e).ShouldBe((DateTime.MinValue, Now));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_forward_window_starts_at_the_floor()
|
|
{
|
|
var (s, e) = (DateTime.UnixEpoch, Now);
|
|
EventHorizon.TryClamp(ref s, ref e, reverse: false, H, Now).ShouldBeTrue();
|
|
(s, e).ShouldBe((Floor, Now));
|
|
|
|
var (s2, e2) = (Floor.AddDays(1), Now);
|
|
EventHorizon.TryClamp(ref s2, ref e2, reverse: false, H, Now).ShouldBeTrue();
|
|
s2.ShouldBe(Floor.AddDays(1)); // already inside: unchanged
|
|
}
|
|
|
|
[Fact]
|
|
public void A_reverse_window_keeps_the_floor_instant()
|
|
{
|
|
var (s, e) = (Now, new DateTime(0, DateTimeKind.Utc));
|
|
EventHorizon.TryClamp(ref s, ref e, reverse: true, H, Now).ShouldBeTrue();
|
|
(s, e).ShouldBe((Now, Floor.AddTicks(-1))); // (end, start] — the floor itself is read
|
|
}
|
|
|
|
[Fact]
|
|
public void A_window_wholly_before_the_floor_is_empty()
|
|
{
|
|
var (s, e) = (Floor.AddDays(-2), Floor);
|
|
EventHorizon.TryClamp(ref s, ref e, reverse: false, H, Now).ShouldBeFalse(); // [start, end) ends at the floor
|
|
|
|
var (rs, re) = (Floor.AddTicks(-1), Floor.AddDays(-2));
|
|
EventHorizon.TryClamp(ref rs, ref re, reverse: true, H, Now).ShouldBeFalse();
|
|
|
|
var (ok, oe) = (Floor, Floor.AddDays(-2));
|
|
EventHorizon.TryClamp(ref ok, ref oe, reverse: true, H, Now).ShouldBeTrue(); // (…, floor] holds the floor
|
|
}
|
|
|
|
[Fact]
|
|
public void A_horizon_longer_than_the_calendar_is_no_horizon()
|
|
{
|
|
var (s, e) = (DateTime.MinValue, Now);
|
|
EventHorizon.TryClamp(ref s, ref e, reverse: false, TimeSpan.MaxValue, Now).ShouldBeTrue();
|
|
s.ShouldBe(DateTime.MinValue);
|
|
}
|
|
}
|