11b703708c
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
141 lines
5.1 KiB
C#
141 lines
5.1 KiB
C#
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");
|
|
}
|
|
}
|