using System.Net.Sockets; using Npgsql; using NpgsqlTypes; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests; public sealed class QuestDbConnectionTests { private static DateTime Bound(DateTime v, bool clamp = false) { var cmd = new NpgsqlCommand(); var p = QuestDbConnection.BindTimestamp(cmd, v, clamp); p.NpgsqlDbType.ShouldBe(NpgsqlDbType.TimestampTz); var bound = (DateTime)p.Value!; bound.Kind.ShouldBe(DateTimeKind.Utc); return bound; } [Fact] public void Round_truncates_sub_microsecond_ticks() { var t = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc).AddTicks(1234567); QuestDbConnection.Round(t).ShouldBe(new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc).AddTicks(1234560)); } [Fact] public void Round_truncates_rather_than_rounds_before_2000() { var t = new DateTime(1990, 5, 5, 0, 0, 0, DateTimeKind.Utc).AddTicks(1234565); QuestDbConnection.Round(t).Ticks.ShouldBe(t.Ticks - 5); } [Fact] public void Round_treats_Unspecified_as_UTC() { var u = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Unspecified); var r = QuestDbConnection.Round(u); r.Kind.ShouldBe(DateTimeKind.Utc); r.Ticks.ShouldBe(u.Ticks); } [Fact] public void Round_converts_Local_to_UTC() { var local = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Local); var r = QuestDbConnection.Round(local); r.Kind.ShouldBe(DateTimeKind.Utc); r.ShouldBe(local.ToUniversalTime()); } [Fact] public void Round_keeps_a_UTC_value_that_is_already_aligned() { var t = new DateTime(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc); QuestDbConnection.Round(t).ShouldBe(t); } [Theory] [InlineData(0)] // MinValue itself [InlineData(5)] // truncates to MinValue public void BindTimestamp_never_emits_MinValue(long ticks) { var b = Bound(new DateTime(ticks, DateTimeKind.Utc)); b.ShouldNotBe(DateTime.MinValue); b.Ticks.ShouldBe(10); b.ShouldBe(new DateTime(10, DateTimeKind.Utc)); // 0001-01-01T00:00:00.000001 (T0-confirmed) } [Fact] public void BindTimestamp_never_emits_MaxValue() { var b = Bound(DateTime.MaxValue); b.ShouldNotBe(DateTime.MaxValue); b.ShouldBe(new DateTime(9999, 12, 31, 23, 59, 59, DateTimeKind.Utc).AddTicks(9999990)); // T0-confirmed bound Bound(DateTime.MinValue).ShouldNotBe(DateTime.MinValue); } [Fact] public void BindTimestamp_never_emits_either_extreme_for_any_kind() { foreach (var kind in new[] { DateTimeKind.Utc, DateTimeKind.Unspecified, DateTimeKind.Local }) foreach (var t in new[] { new DateTime(DateTime.MinValue.Ticks, kind), new DateTime(DateTime.MaxValue.Ticks, kind) }) { var b = Bound(t); b.ShouldNotBe(DateTime.MinValue); b.ShouldNotBe(DateTime.MaxValue); } } [Fact] public void BindTimestamp_keeps_a_pre_1970_range_bound_unless_asked_to_clamp() { var pre = new DateTime(1960, 1, 1, 0, 0, 0, DateTimeKind.Utc); Bound(pre).ShouldBe(pre); Bound(pre, clamp: true).ShouldBe(DateTime.UnixEpoch); Bound(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), clamp: true).Year.ShouldBe(2026); } [Fact] public void IsStorable_is_false_only_before_1970() { QuestDbConnection.IsStorable(DateTime.UnixEpoch).ShouldBeTrue(); QuestDbConnection.IsStorable(DateTime.UnixEpoch.AddTicks(-1)).ShouldBeFalse(); QuestDbConnection.IsStorable(new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ShouldBeFalse(); QuestDbConnection.IsStorable(DateTime.MinValue).ShouldBeFalse(); QuestDbConnection.IsStorable(DateTime.MaxValue).ShouldBeTrue(); QuestDbConnection.IsStorable(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ShouldBeTrue(); } [Fact] public void BindByte_binds_smallint() { var p = QuestDbConnection.BindByte(new NpgsqlCommand(), 200); p.NpgsqlDbType.ShouldBe(NpgsqlDbType.Smallint); p.Value.ShouldBe((short)200); } [Fact] public void IsTransient_classifies_transport_failures_not_server_rejections() { QuestDbConnection.IsTransient(new SocketException()).ShouldBeTrue(); QuestDbConnection.IsTransient(new IOException("x")).ShouldBeTrue(); QuestDbConnection.IsTransient(new TimeoutException()).ShouldBeTrue(); QuestDbConnection.IsTransient(new NpgsqlException("x", new IOException())).ShouldBeTrue(); QuestDbConnection.IsTransient(new InvalidOperationException("x")).ShouldBeFalse(); QuestDbConnection.IsTransient(new OperationCanceledException()).ShouldBeFalse(); } [Fact] public void T_prefixes_the_table_base_name() { var c = new QuestDbConnection(new QuestDbHistorianOptions { ConnectionString = "Host=127.0.0.1;Port=1", TablePrefix = "p_" }); c.T("sample").ShouldBe("p_sample"); c.SampleTable.ShouldBe("p_sample"); c.EventTable.ShouldBe("p_event"); c.TagTable.ShouldBe("p_tag"); } // ---- pool size (final review I-1) ------------------------------------------------------------------------------- private static int PoolOf(string cs) { using var c = new QuestDbConnection(new QuestDbHistorianOptions { ConnectionString = cs }); return c.EffectiveMaxPoolSize; } [Fact] public void An_unset_Maximum_Pool_Size_is_capped_at_24() => PoolOf("Host=127.0.0.1;Port=1").ShouldBe(24); [Fact] public void The_default_cap_constant_is_24() => QuestDbConnection.DefaultMaxPoolSize.ShouldBe(24); [Theory] [InlineData("Host=127.0.0.1;Port=1;Maximum Pool Size=10", 10)] [InlineData("Host=127.0.0.1;Port=1;Maximum Pool Size=80", 80)] [InlineData("Host=127.0.0.1;Port=1;maximum pool size=30", 30)] [InlineData("Host=127.0.0.1;Port=1;MaxPoolSize=80", 80)] public void An_explicit_Maximum_Pool_Size_is_preserved(string cs, int expected) => PoolOf(cs).ShouldBe(expected); [Fact] public void The_effective_pool_size_is_logged_once_at_Information() { var log = new InfoLogger(); using var c = new QuestDbConnection(new QuestDbHistorianOptions { ConnectionString = "Host=127.0.0.1;Port=1" }, log); log.Messages.Count.ShouldBe(1); log.Messages[0].ShouldContain("24"); } private sealed class InfoLogger : Microsoft.Extensions.Logging.ILogger { public List Messages { get; } = []; public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => true; public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, Exception? exception, Func formatter) { if (logLevel == Microsoft.Extensions.Logging.LogLevel.Information) Messages.Add(formatter(state, exception)); } } }