using Microsoft.Extensions.Logging; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests; /// /// The pre-1970 drop Warning is rate-limited per tag (final review M-3) while the counter still counts every dropped /// sample. All-pre-1970 batches return before any QuestDB call, so the connection points at a closed port. /// public sealed class QuestDbHistorianValueWriterTests { private static readonly DateTime FileTimeZero = new(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static (QuestDbHistorianValueWriter W, ListLogger Log, Action Advance) Make() { var opts = new QuestDbHistorianOptions { ConnectionString = "Host=127.0.0.1;Port=1;Timeout=1" }; var conn = new QuestDbConnection(opts); var boot = new QuestDbSchemaBootstrapper(conn, opts); var wal = new QuestDbWalVisibility(conn, opts.VisibilityTimeout); var now = new DateTime(2026, 10, 11, 12, 0, 0, DateTimeKind.Utc); var log = new ListLogger(); var w = new QuestDbHistorianValueWriter(conn, new QuestDbTagCatalog(conn, boot, wal), wal, log, () => now); return (w, log, d => now += d); } private static HistorizationValue[] Pre1970(int n) => Enumerable.Range(0, n).Select(i => new HistorizationValue(FileTimeZero.AddSeconds(i), HistorianValue.FromDouble(i), 0)).ToArray(); [Fact] public async Task The_pre_1970_warning_is_logged_once_per_tag_per_interval_but_every_drop_is_counted() { var (w, log, advance) = Make(); (await w.WriteLiveValuesAsync("a", Pre1970(2), CancellationToken.None)).ShouldBeTrue(); advance(TimeSpan.FromSeconds(2)); (await w.WriteLiveValuesAsync("a", Pre1970(3), CancellationToken.None)).ShouldBeTrue(); advance(QuestDbHistorianValueWriter.Pre1970WarningInterval - TimeSpan.FromSeconds(3)); (await w.WriteLiveValuesAsync("a", Pre1970(1), CancellationToken.None)).ShouldBeTrue(); log.Warnings.ShouldBe(1); w.DroppedPre1970.ShouldBe(6); // A different tag has its own budget. (await w.WriteLiveValuesAsync("b", Pre1970(1), CancellationToken.None)).ShouldBeTrue(); log.Warnings.ShouldBe(2); // Once the interval has passed since the last Warning for "a", it warns again. advance(TimeSpan.FromSeconds(1)); (await w.WriteLiveValuesAsync("a", Pre1970(1), CancellationToken.None)).ShouldBeTrue(); log.Warnings.ShouldBe(3); w.DroppedPre1970.ShouldBe(8); } [Fact] public void The_interval_is_ten_minutes() => QuestDbHistorianValueWriter.Pre1970WarningInterval.ShouldBe(TimeSpan.FromMinutes(10)); private sealed class ListLogger : ILogger { private int _warnings; public int Warnings => Volatile.Read(ref _warnings); public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (logLevel == LogLevel.Warning) Interlocked.Increment(ref _warnings); } } }