using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Conformance; /// Raw-read behaviour over the fixture tag A (see ). public abstract class RawReadConformance : ConformanceBase { private static readonly CancellationToken Ct = CancellationToken.None; private static double?[] Values(HistoryReadResult r) => r.Samples.Select(s => (double?)s.Value).ToArray(); [Fact] public async Task Forward_read_is_start_inclusive_end_exclusive_ascending_with_exact_ticks() { await ConformanceData.SeedAAsync(B); var r = await B.CreateDataSource().ReadRawAsync("A", At(10), At(40), 0, Ct); Values(r).ShouldBe([20d, 30d, 40d]); // +10 included, +40 (== end) excluded r.Samples.Select(s => s.SourceTimestampUtc!.Value.Ticks).ShouldBe([B.Round(At(10)).Ticks, B.Round(At(20)).Ticks, B.Round(At(30)).Ticks]); r.Samples.Select(s => s.StatusCode).ShouldBe([Good, Bad, Good]); r.Samples[0].SourceTimestampUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc); r.Samples[0].ServerTimestampUtc.Ticks.ShouldBe(B.Round(At(11)).Ticks); // stored ServerTimeUtc, not "now" r.ContinuationPoint.ShouldBeNull(); } [Fact] public async Task Forward_read_honours_the_limit() { await ConformanceData.SeedAAsync(B); Values(await B.CreateDataSource().ReadRawAsync("A", At(0), At(100), 2, Ct)).ShouldBe([10d, 20d]); } [Fact] public async Task Zero_max_values_is_unbounded() { await ConformanceData.SeedAAsync(B); Values(await B.CreateDataSource().ReadRawAsync("A", At(-1), At(100), 0, Ct)).ShouldBe([10d, 20d, 30d, 40d, 50d]); Values(await B.CreateDataSource().ReadRawAsync("A", At(-1), At(100), uint.MaxValue, Ct)).Length.ShouldBe(5); } [Fact] public async Task Reverse_read_is_descending_over_end_exclusive_start_inclusive() { await ConformanceData.SeedAAsync(B); // start=+30, end=+10 ⇒ (+10, +30] descending. Values(await B.CreateDataSource().ReadRawAsync("A", At(30), At(10), 0, Ct)).ShouldBe([40d, 30d]); Values(await B.CreateDataSource().ReadRawAsync("A", At(100), At(-1), 2, Ct)).ShouldBe([50d, 40d]); } [Fact] public async Task Equal_start_and_end_is_a_point_read() { await ConformanceData.SeedAAsync(B); Values(await B.CreateDataSource().ReadRawAsync("A", At(10), At(10), 0, Ct)).ShouldBe([20d]); // One resolution unit later: the stored sample is no longer at that instant. var later = At(10).AddTicks(B.ResolutionTicks); (await B.CreateDataSource().ReadRawAsync("A", later, later, 0, Ct)).Samples.ShouldBeEmpty(); } [Fact] public async Task A_resumed_page_from_the_last_timestamp_inclusive_continues_without_gaps() { // The node manager's paging: read cap N, resume at the last returned timestamp INCLUSIVE, trim it. await ConformanceData.SeedAAsync(B); var src = B.CreateDataSource(); var p1 = await src.ReadRawAsync("A", At(0), At(100), 2, Ct); var p2 = await src.ReadRawAsync("A", p1.Samples[^1].SourceTimestampUtc!.Value, At(100), 2, Ct); Values(p1).ShouldBe([10d, 20d]); Values(p2).ShouldBe([20d, 30d]); // first is the boundary sample the node manager trims } [Fact] public async Task Unknown_tag_is_an_empty_result_and_reads_never_create_tags() { var src = B.CreateDataSource(); (await src.ReadRawAsync("nope", At(0), At(10), 0, Ct)).Samples.ShouldBeEmpty(); await src.ReadAtTimeAsync("nope", [At(1)], Ct); await src.ReadProcessedAsync("nope", At(0), At(10), TimeSpan.FromSeconds(5), HistoryAggregateType.Average, Ct); (await B.TagExistsAsync("nope")).ShouldBeFalse(); src.GetHealthSnapshot().TotalFailures.ShouldBe(0); } [Fact] public async Task Outage_markers_and_bad_samples_come_back_with_their_bad_status() { await B.SeedAsync("M", (At(0), HistorianValue.FromDouble(10), Good), (At(5), HistorianValue.Empty, BadNoCommunication), (At(7), HistorianValue.FromDouble(99), Bad)); var r = await B.CreateDataSource().ReadRawAsync("M", At(0), At(10), 0, Ct); r.Samples.Count.ShouldBe(3); r.Samples[1].Value.ShouldBeNull(); r.Samples[1].StatusCode.ShouldBe(0x80310000u); r.Samples[2].Value.ShouldBe(99d); r.Samples[2].StatusCode.ShouldBe(0x80000000u); } [Fact] public async Task Every_kind_round_trips_to_its_CLR_type() { var dt = new DateTime(2025, 1, 2, 3, 4, 5, DateTimeKind.Utc).AddTicks(7 * B.ResolutionTicks); await B.SeedAsync("K", (At(0), HistorianValue.FromInt64(-9_000_000_000L), Good), (At(1), HistorianValue.FromBoolean(true), Good), (At(2), HistorianValue.FromString("héllo"), Good), (At(3), HistorianValue.FromDateTime(dt), Good), (At(4), HistorianValue.FromDouble(1.5), Uncertain)); var s = (await B.CreateDataSource().ReadRawAsync("K", At(0), At(5), 0, Ct)).Samples; s[0].Value.ShouldBe(-9_000_000_000L); s[1].Value.ShouldBe(true); s[2].Value.ShouldBe("héllo"); s[3].Value.ShouldBe(dt); ((DateTime)s[3].Value!).Kind.ShouldBe(DateTimeKind.Utc); s[4].Value.ShouldBe(1.5); s[4].StatusCode.ShouldBe(0x40000000u); } [Fact] public async Task Successful_reads_are_counted_in_health() { await ConformanceData.SeedAAsync(B); var src = B.CreateDataSource(); await src.ReadRawAsync("A", At(0), At(10), 0, Ct); await src.ReadRawAsync("A", At(0), At(10), 0, Ct); var h = src.GetHealthSnapshot(); h.TotalQueries.ShouldBe(2); h.TotalSuccesses.ShouldBe(2); h.ConsecutiveFailures.ShouldBe(0); h.LastSuccessTime.ShouldNotBeNull(); h.ProcessConnectionOpen.ShouldBeTrue(); } }