using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Conformance; /// /// Gitea #691: the server composes Part 11 bounding values (HistoryRead-Raw ReturnBounds) from exactly these raw /// reads on a backend that reads newest-first natively (HistoryBounds in the OPC UA server) — so every backend must /// answer them precisely: "at or before T" = a reverse read of (0, T] capped at one, "at or after T" = a forward read /// of [T, MaxValue) capped at one, "strictly after T" = the same from T + resolution. A bound is a stored /// sample of any quality, so a Bad row or an outage marker comes back as the bound. /// public abstract class BoundingValueConformance : ConformanceBase { private static readonly CancellationToken Ct = CancellationToken.None; private static readonly DateTime Floor = new(0, DateTimeKind.Utc); private static readonly DateTime Ceiling = new(DateTime.MaxValue.Ticks, DateTimeKind.Utc); private async Task AtOrBeforeAsync(IHistorianDataSource src, string tag, DateTime t) => (await src.ReadRawAsync(tag, t, Floor, 1, Ct)).Samples.SingleOrDefault(); private async Task AtOrAfterAsync(IHistorianDataSource src, string tag, DateTime t) => (await src.ReadRawAsync(tag, t, Ceiling, 1, Ct)).Samples.SingleOrDefault(); [Fact] public async Task The_sample_at_or_before_a_time_is_the_latest_one_on_or_before_it() { await ConformanceData.SeedAAsync(B); var src = B.CreateDataSource(); (await AtOrBeforeAsync(src, "A", At(10)))!.Value.ShouldBe(20d); // exactly on it (await AtOrBeforeAsync(src, "A", At(15)))!.Value.ShouldBe(20d); // between samples (await AtOrBeforeAsync(src, "A", At(1000)))!.Value.ShouldBe(50d); // past the last (await AtOrBeforeAsync(src, "A", At(-1))).ShouldBeNull(); // before the first: no bound (await AtOrBeforeAsync(src, "A", At(15)))!.SourceTimestampUtc!.Value.Ticks.ShouldBe(B.Round(At(10)).Ticks); } [Fact] public async Task The_sample_at_or_after_a_time_is_the_earliest_one_on_or_after_it() { await ConformanceData.SeedAAsync(B); var src = B.CreateDataSource(); (await AtOrAfterAsync(src, "A", At(10)))!.Value.ShouldBe(20d); // exactly on it (await AtOrAfterAsync(src, "A", At(15)))!.Value.ShouldBe(30d); // between samples (await AtOrAfterAsync(src, "A", At(-100)))!.Value.ShouldBe(10d); // before the first (await AtOrAfterAsync(src, "A", At(41))).ShouldBeNull(); // past the last: no bound } [Fact] public async Task The_sample_strictly_after_a_time_starts_one_storage_resolution_later() { await ConformanceData.SeedAAsync(B); var src = B.CreateDataSource(); (await AtOrAfterAsync(src, "A", At(10).AddTicks(B.ResolutionTicks)))!.Value.ShouldBe(30d); (await AtOrAfterAsync(src, "A", At(40).AddTicks(B.ResolutionTicks))).ShouldBeNull(); } [Fact] public async Task A_time_between_storage_ticks_finds_the_sample_the_backend_stores_at_its_truncation() { // The server compares "on the bound" at the storage resolution, so the lookups must agree with the window reads: // a time a fraction of a resolution step after a stored sample is that sample's instant on a coarse backend. await ConformanceData.SeedAAsync(B); var src = B.CreateDataSource(); var t = At(10).AddTicks(B.ResolutionTicks - 1); // == At(10) on a 1-tick backend (await AtOrBeforeAsync(src, "A", t))!.Value.ShouldBe(20d); var after = await AtOrAfterAsync(src, "A", t); after!.SourceTimestampUtc!.Value.Ticks.ShouldBeGreaterThanOrEqualTo(B.Round(t).Ticks); after.Value.ShouldBe(B.Round(t) == B.Round(At(10)) ? 20d : 30d); } [Fact] public async Task A_bad_sample_or_an_outage_marker_is_a_bound_as_stored() { await B.SeedAsync("M", (At(0), HistorianValue.FromDouble(10), Good), (At(5), HistorianValue.Empty, BadNoCommunication), (At(7), HistorianValue.FromDouble(99), Bad)); var src = B.CreateDataSource(); var before = await AtOrBeforeAsync(src, "M", At(6)); before!.Value.ShouldBeNull(); before.StatusCode.ShouldBe(0x80310000u); var after = await AtOrAfterAsync(src, "M", At(6)); after!.Value.ShouldBe(99d); after.StatusCode.ShouldBe(0x80000000u); } [Fact] public async Task An_unknown_tag_has_no_bounds() { var src = B.CreateDataSource(); (await AtOrBeforeAsync(src, "nope", At(10))).ShouldBeNull(); (await AtOrAfterAsync(src, "nope", At(10))).ShouldBeNull(); } }