test(historian): backend conformance suite (shared source), run against SQL Server

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-11 01:50:51 -04:00
parent ca2c20f852
commit c5b33aa123
16 changed files with 847 additions and 639 deletions
@@ -37,101 +37,6 @@ public sealed class WriteTests(SqlHistorianFixture fx)
return l;
}
[Fact]
public async Task Every_kind_round_trips_with_exact_ticks_and_status()
{
fx.SkipIfUnavailable();
var (w, s) = Make();
var dt = new DateTime(2026, 1, 2, 3, 4, 5, DateTimeKind.Utc).AddTicks(77);
HistorizationValue[] vals =
[
new(T0, HistorianValue.FromDouble(1.25), 0),
new(T0.AddTicks(1), HistorianValue.FromInt64(long.MaxValue), 0x40000000u),
new(T0.AddTicks(2), HistorianValue.FromBoolean(true), 0),
new(T0.AddTicks(3), HistorianValue.FromString("héllo"), 0),
new(T0.AddTicks(4), HistorianValue.FromDateTime(dt), 0),
new(T0.AddTicks(5), HistorianValue.Empty, 0x80310000u),
];
(await w.WriteLiveValuesAsync("tag.a", vals, CancellationToken.None)).ShouldBeTrue();
var rows = await RowsAsync(s, "tag.a");
rows.Count.ShouldBe(6);
for (var i = 0; i < 6; i++) rows[i].T.Ticks.ShouldBe(vals[i].TimestampUtc!.Value.Ticks);
var back = rows.Select(r => SampleColumnMapper.FromColumns(r.Kind, r.N, r.I, r.S)).ToList();
back.ShouldBe(vals.Select(v => v.Value).ToList());
(rows[5].N, rows[5].I, rows[5].S).ShouldBe((null, null, null));
rows[1].Status.ShouldBe(unchecked((int)0x40000000u));
SampleColumnMapper.FromDbStatus(rows[5].Status).ShouldBe(0x80310000u);
}
[Fact]
public async Task Writing_the_same_batch_twice_is_idempotent()
{
fx.SkipIfUnavailable();
var (w, s) = Make();
HistorizationValue[] vals = [new(T0, HistorianValue.FromDouble(1), 0), new(T0.AddSeconds(1), HistorianValue.FromDouble(2), 0)];
(await w.WriteLiveValuesAsync("tag.b", vals, CancellationToken.None)).ShouldBeTrue();
(await w.WriteLiveValuesAsync("tag.b", vals, CancellationToken.None)).ShouldBeTrue();
// Overlapping batch: only the new row lands, the stored first value is untouched.
(await w.WriteLiveValuesAsync("tag.b", [new(T0, HistorianValue.FromDouble(99), 0), new(T0.AddSeconds(2), HistorianValue.FromDouble(3), 0)], CancellationToken.None)).ShouldBeTrue();
var rows = await RowsAsync(s, "tag.b");
rows.Count.ShouldBe(3);
rows[0].N.ShouldBe(1);
}
[Fact]
public async Task Within_batch_duplicate_timestamps_do_not_fail_and_first_wins()
{
fx.SkipIfUnavailable();
var (w, s) = Make();
(await w.WriteLiveValuesAsync("tag.c", [new(T0, HistorianValue.FromDouble(1), 0), new(T0, HistorianValue.FromDouble(2), 0)], CancellationToken.None)).ShouldBeTrue();
var rows = await RowsAsync(s, "tag.c");
rows.Count.ShouldBe(1);
rows[0].N.ShouldBe(1);
}
[Fact]
public async Task Non_finite_doubles_store_as_Empty_forced_Bad_and_do_not_fail_the_write()
{
fx.SkipIfUnavailable();
var (w, s) = Make();
(await w.WriteLiveValuesAsync("tag.d", [
new(T0, HistorianValue.FromDouble(double.NaN), 0x40000000u),
new(T0.AddTicks(1), HistorianValue.FromDouble(double.PositiveInfinity), 0),
new(T0.AddTicks(2), HistorianValue.FromDouble(7), 0)], CancellationToken.None)).ShouldBeTrue();
var rows = await RowsAsync(s, "tag.d");
rows.Count.ShouldBe(3);
rows[0].Kind.ShouldBe((byte)HistorianValueKind.Empty);
rows[0].N.ShouldBeNull();
rows[0].Status.ShouldBe(unchecked((int)HistorianStatusCodes.BadOutOfRange)); // Uncertain NaN ⇒ Bad (I-1)
rows[1].Status.ShouldBe(unchecked((int)HistorianStatusCodes.BadOutOfRange)); // Good +Inf ⇒ Bad (I-1)
rows[2].N.ShouldBe(7);
}
[Fact]
public async Task Empty_batch_succeeds_without_a_call_and_null_timestamp_is_stamped()
{
fx.SkipIfUnavailable();
var (w, s) = Make();
(await w.WriteLiveValuesAsync("tag.e", [], CancellationToken.None)).ShouldBeTrue();
(await w.WriteLiveValuesAsync("tag.e", [new(null, HistorianValue.FromInt64(5), 0)], CancellationToken.None)).ShouldBeTrue();
var rows = await RowsAsync(s, "tag.e");
rows.Count.ShouldBe(1);
(DateTime.UtcNow - rows[0].T).ShouldBeLessThan(TimeSpan.FromMinutes(1));
}
[Fact]
public async Task Tag_is_created_with_the_first_typed_values_data_type()
{
fx.SkipIfUnavailable();
var (w, s) = Make();
await w.WriteLiveValuesAsync("tag.f", [new(T0, HistorianValue.Empty, 0x80310000u), new(T0.AddTicks(1), HistorianValue.FromString("x"), 0)], CancellationToken.None);
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await using var cmd = c.CreateCommand();
cmd.CommandText = $"SELECT DataType FROM [{s}].Tag WHERE Name='tag.f'";
Convert.ToByte(await cmd.ExecuteScalarAsync()).ShouldBe((byte)DriverDataType.String);
}
[Fact]
public async Task Ten_thousand_values_write_in_under_five_seconds()
{