using System.Text; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests; [Collection(SqlHistorianCollection.Name)] public sealed class EventReadTests(SqlHistorianFixture fx) { private static readonly CancellationToken Ct = CancellationToken.None; private static DateTime At(double s) => ReadTestDb.At(s); private static async Task InsertEventAsync(ReadTestDb db, string id, string source, DateTime t, string? message, int severity) { await using var c = await db.Conn.OpenAsync(Ct); await using var cmd = db.Conn.CreateCommand(c, $"INSERT {db.Conn.Q("Event")} (EventId, SourceName, AlarmId, AlarmName, AlarmTypeName, Severity, EventKind, Message, " + "UserName, Comment, EventTimeUtc, ReceivedTimeUtc) VALUES (@id, @src, 'a', 'n', 'T', @sev, 'Activated', @msg, NULL, NULL, @t, @r)"); cmd.Parameters.AddWithValue("@id", id); cmd.Parameters.AddWithValue("@src", source); cmd.Parameters.AddWithValue("@sev", severity); cmd.Parameters.AddWithValue("@msg", (object?)message ?? DBNull.Value); SqlHistorianConnection.AddDateTime2(cmd, "@t", t); SqlHistorianConnection.AddDateTime2(cmd, "@r", t.AddTicks(3)); await cmd.ExecuteNonQueryAsync(); } private static async Task SeedAsync(SqlHistorianFixture fx) { var db = await ReadTestDb.CreateAsync(fx); await InsertEventAsync(db, "e1", "Eq/1", At(0), "first", 500); await InsertEventAsync(db, "e2", "Eq/2", At(1), null, 700); await InsertEventAsync(db, "e3", "Eq/1", At(2), "third", 900); await InsertEventAsync(db, "e4", "Eq/1", At(3), "at end", 100); return db; } [Fact] public async Task Source_filter_window_order_and_field_mapping() { var db = await SeedAsync(fx); var r = await db.Source().ReadEventsAsync("Eq/1", At(0), At(3), 0, Ct); r.Events.Select(e => e.EventId).ShouldBe(["e1", "e3"]); // e4 at end excluded, e2 other source var e = r.Events[0]; e.SourceName.ShouldBe("Eq/1"); e.EventTimeUtc.ShouldBe(At(0)); e.EventTimeUtc.Kind.ShouldBe(DateTimeKind.Utc); e.ReceivedTimeUtc.ShouldBe(At(0).AddTicks(3)); e.Message.ShouldBe("first"); e.Severity.ShouldBe((ushort)500); r.ContinuationPoint.ShouldBeNull(); } [Fact] public async Task Null_source_returns_every_source_and_null_messages() { var db = await SeedAsync(fx); var r = await db.Source().ReadEventsAsync(null, At(0), At(10), 0, Ct); r.Events.Select(e => e.EventId).ShouldBe(["e1", "e2", "e3", "e4"]); r.Events[1].Message.ShouldBeNull(); } [Fact] public async Task Truncation_drops_the_extra_row_and_returns_a_continuation_point() { var db = await SeedAsync(fx); var r = await db.Source().ReadEventsAsync(null, At(0), At(3), 2, Ct); // 3 matching rows r.Events.Select(e => e.EventId).ShouldBe(["e1", "e2"]); r.ContinuationPoint.ShouldNotBeNull(); Encoding.UTF8.GetString(r.ContinuationPoint!).ShouldBe($"{At(2).Ticks}|e3"); } [Fact] public async Task An_exact_fit_or_an_unbounded_read_has_no_continuation_point() { var db = await SeedAsync(fx); var src = db.Source(); var exact = await src.ReadEventsAsync(null, At(0), At(3), 3, Ct); exact.Events.Count.ShouldBe(3); exact.ContinuationPoint.ShouldBeNull(); (await src.ReadEventsAsync(null, At(0), At(10), int.MaxValue, Ct)).Events.Count.ShouldBe(4); // node manager's "no limit" (await src.ReadEventsAsync(null, At(0), At(10), -1, Ct)).ContinuationPoint.ShouldBeNull(); } [Fact] public async Task A_reversed_window_is_swapped() { var db = await SeedAsync(fx); (await db.Source().ReadEventsAsync(null, At(3), At(0), 0, Ct)).Events.Select(e => e.EventId) .ShouldBe(["e1", "e2", "e3"]); } }