feat(historian): typed HistorianValue contracts, IHistorianDataUpdater, IHistorianBackendProvider (no behaviour change)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 12:44:19 -04:00
parent a6643cedf9
commit ff7c6c531a
15 changed files with 357 additions and 65 deletions
@@ -0,0 +1,114 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.Historian;
public sealed class HistorianValueTests
{
[Theory]
[InlineData(1.5d, HistorianValueKind.Double)]
[InlineData(1.5f, HistorianValueKind.Double)]
[InlineData(7, HistorianValueKind.Int64)]
[InlineData((short)7, HistorianValueKind.Int64)]
[InlineData((byte)7, HistorianValueKind.Int64)]
[InlineData(7u, HistorianValueKind.Int64)]
[InlineData(7L, HistorianValueKind.Int64)]
[InlineData(true, HistorianValueKind.Boolean)]
[InlineData("x", HistorianValueKind.String)]
public void Maps_clr_values_to_kinds(object input, HistorianValueKind expected)
{
HistorianValue.TryFromObject(input, out var v).ShouldBeTrue();
v.Kind.ShouldBe(expected);
}
[Fact]
public void Decimal_maps_to_double()
{
HistorianValue.TryFromObject(2.5m, out var v).ShouldBeTrue();
v.Kind.ShouldBe(HistorianValueKind.Double);
v.Number.ShouldBe(2.5);
}
[Fact]
public void Ulong_above_long_max_maps_to_double()
{
HistorianValue.TryFromObject(ulong.MaxValue, out var v).ShouldBeTrue();
v.Kind.ShouldBe(HistorianValueKind.Double);
v.Number.ShouldBe((double)ulong.MaxValue);
}
[Fact]
public void Ulong_within_long_range_maps_to_int64()
{
HistorianValue.TryFromObject((ulong)5, out var v).ShouldBeTrue();
v.Kind.ShouldBe(HistorianValueKind.Int64);
v.Integer.ShouldBe(5);
HistorianValue.TryFromObject((ulong)long.MaxValue, out var edge).ShouldBeTrue();
edge.Kind.ShouldBe(HistorianValueKind.Int64);
}
[Fact]
public void Null_maps_to_empty()
{
HistorianValue.TryFromObject(null, out var v).ShouldBeTrue();
v.Kind.ShouldBe(HistorianValueKind.Empty);
v.ToObject().ShouldBeNull();
}
[Fact]
public void Arrays_and_unknown_objects_are_not_historizable()
{
HistorianValue.TryFromObject(new[] { 1, 2 }, out _).ShouldBeFalse();
HistorianValue.TryFromObject(new object(), out _).ShouldBeFalse();
}
[Fact]
public void Local_datetime_is_stored_as_utc_ticks()
{
var local = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Local);
HistorianValue.TryFromObject(local, out var v).ShouldBeTrue();
v.Kind.ShouldBe(HistorianValueKind.DateTime);
v.Integer.ShouldBe(local.ToUniversalTime().Ticks);
var back = (DateTime)v.ToObject()!;
back.Kind.ShouldBe(DateTimeKind.Utc);
back.Ticks.ShouldBe(local.ToUniversalTime().Ticks);
}
[Fact]
public void Boolean_carries_integer_and_number()
{
var v = HistorianValue.FromBoolean(true);
v.Integer.ShouldBe(1);
v.Number.ShouldBe(1d);
v.TryGetDouble(out var d).ShouldBeTrue();
d.ShouldBe(1d);
HistorianValue.FromBoolean(false).Integer.ShouldBe(0);
}
[Fact]
public void TryGetDouble_is_true_only_for_double_int64_boolean()
{
HistorianValue.FromDouble(1.5).TryGetDouble(out _).ShouldBeTrue();
HistorianValue.FromInt64(3).TryGetDouble(out var i).ShouldBeTrue();
i.ShouldBe(3d);
HistorianValue.FromBoolean(true).TryGetDouble(out _).ShouldBeTrue();
HistorianValue.FromString("a").TryGetDouble(out _).ShouldBeFalse();
HistorianValue.FromDateTime(DateTime.UtcNow).TryGetDouble(out _).ShouldBeFalse();
HistorianValue.Empty.TryGetDouble(out _).ShouldBeFalse();
}
[Fact]
public void ToObject_round_trips_each_kind()
{
HistorianValue.FromDouble(1.5).ToObject().ShouldBe(1.5);
HistorianValue.FromInt64(9).ToObject().ShouldBe(9L);
HistorianValue.FromBoolean(true).ToObject().ShouldBe(true);
HistorianValue.FromBoolean(false).ToObject().ShouldBe(false);
HistorianValue.FromString("a").ToObject().ShouldBe("a");
var utc = new DateTime(2026, 5, 6, 7, 8, 9, DateTimeKind.Utc);
var dt = (DateTime)HistorianValue.FromDateTime(utc).ToObject()!;
dt.ShouldBe(utc);
dt.Kind.ShouldBe(DateTimeKind.Utc);
HistorianValue.Empty.ToObject().ShouldBeNull();
}
}