feat(historian): typed HistorianValue contracts, IHistorianDataUpdater, IHistorianBackendProvider (no behaviour change)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+4
-4
@@ -87,7 +87,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
// A value that is exactly representable in float32 (integer < 2^24) so the analog
|
||||
// store/read round-trip is not muddied by single-precision rounding. The millisecond
|
||||
// component keeps consecutive runs from colliding on the same value.
|
||||
const ushort goodQuality = 192; // OPC-DA "Good" floor.
|
||||
const uint goodStatus = 0u; // OPC UA Good.
|
||||
var writeUtc = DateTime.UtcNow;
|
||||
double written = 1_000_000 + writeUtc.Millisecond;
|
||||
|
||||
@@ -112,7 +112,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
// WriteLiveValues through the real recorder writer (SQL live-write path).
|
||||
var valueWriter = new GatewayHistorianValueWriter(writeClient, NullLogger<GatewayHistorianValueWriter>.Instance);
|
||||
var acked = await valueWriter.WriteLiveValuesAsync(
|
||||
tag, new[] { new HistorizationValue(writeUtc, written, goodQuality) }, ct);
|
||||
tag, new[] { new HistorizationValue(writeUtc, HistorianValue.FromDouble(written), goodStatus) }, ct);
|
||||
acked.ShouldBeTrue(
|
||||
"the live write must be acked — needs the gateway running RuntimeDb:Enabled=true and the tag EnsureTags-provisioned.");
|
||||
|
||||
@@ -286,12 +286,12 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
|
||||
provision.Failed.ShouldBe(0);
|
||||
|
||||
// WriteLiveValues a Boolean-as-1.0 and read it back (SQL live path can lag a flush cadence → poll).
|
||||
const ushort goodQuality = 192; // OPC-DA "Good" floor.
|
||||
const uint goodStatus = 0u; // OPC UA Good.
|
||||
var writeUtc = DateTime.UtcNow;
|
||||
const double written = 1.0;
|
||||
var valueWriter = new GatewayHistorianValueWriter(writeClient, NullLogger<GatewayHistorianValueWriter>.Instance);
|
||||
var acked = await valueWriter.WriteLiveValuesAsync(
|
||||
tag, new[] { new HistorizationValue(writeUtc, written, goodQuality) }, ct);
|
||||
tag, new[] { new HistorizationValue(writeUtc, HistorianValue.FromDouble(written), goodStatus) }, ct);
|
||||
acked.ShouldBeTrue("the Boolean live write must be acked (gateway RuntimeDb:Enabled=true + EnsureTags-provisioned tag).");
|
||||
|
||||
await using var dataSource = _fx.CreateDataSource();
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ public sealed class FasterLogHistorizationOutboxTests : IDisposable
|
||||
}
|
||||
|
||||
private static HistorizationOutboxEntry E(string tag, double v) =>
|
||||
new(Guid.NewGuid(), tag, v, 192, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
new(Guid.NewGuid(), tag, HistorianValue.FromDouble(v), 0u, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
[Fact]
|
||||
public async Task Append_then_peek_returns_fifo()
|
||||
|
||||
+8
-8
@@ -23,7 +23,7 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
public async Task Success_ack_returns_true()
|
||||
{
|
||||
var fake = new FakeHistorianGatewayClient { WriteLiveValuesResult = new WriteAck { Success = true } };
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, 42.0, 192) };
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, HistorianValue.FromDouble(42.0), 0u) };
|
||||
|
||||
var ok = await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
{
|
||||
WriteLiveValuesResult = new WriteAck { Success = false, Queued = true },
|
||||
};
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, 1.0, 192) };
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, HistorianValue.FromDouble(1.0), 0u) };
|
||||
|
||||
var ok = await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
{
|
||||
WriteLiveValuesResult = new WriteAck { Success = false, Queued = false },
|
||||
};
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, 1.0, 192) };
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, HistorianValue.FromDouble(1.0), 0u) };
|
||||
|
||||
var ok = await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
{
|
||||
// NEVER throw out of the writer — the recorder's drain loop expects a plain bool.
|
||||
var fake = new FakeHistorianGatewayClient { WriteLiveValuesThrows = new InvalidOperationException("boom") };
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, 1.0, 192) };
|
||||
var values = new[] { new HistorizationValue(DateTime.UtcNow, HistorianValue.FromDouble(1.0), 0u) };
|
||||
|
||||
var ok = await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -92,8 +92,8 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
var stamped = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var values = new[]
|
||||
{
|
||||
new HistorizationValue(stamped, 1.0, 192),
|
||||
new HistorizationValue(null, 2.0, 192),
|
||||
new HistorizationValue(stamped, HistorianValue.FromDouble(1.0), 0u),
|
||||
new HistorizationValue(null, HistorianValue.FromDouble(2.0), 0u),
|
||||
};
|
||||
|
||||
await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
|
||||
@@ -108,7 +108,7 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
public async Task Quality_and_numeric_value_are_forwarded_unchanged()
|
||||
{
|
||||
var fake = new FakeHistorianGatewayClient();
|
||||
var values = new[] { new HistorizationValue(null, 123.5, 192) };
|
||||
var values = new[] { new HistorizationValue(null, HistorianValue.FromDouble(123.5), 0u) };
|
||||
|
||||
await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -121,7 +121,7 @@ public sealed class GatewayHistorianValueWriterTests
|
||||
public async Task Null_tag_throws()
|
||||
{
|
||||
var fake = new FakeHistorianGatewayClient();
|
||||
var values = new[] { new HistorizationValue(null, 1.0, 192) };
|
||||
var values = new[] { new HistorizationValue(null, HistorianValue.FromDouble(1.0), 0u) };
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
Writer(fake).WriteLiveValuesAsync(null!, values, TestContext.Current.CancellationToken));
|
||||
|
||||
+9
-10
@@ -18,7 +18,7 @@ public sealed class HistorizationOutboxEntrySerializerTests
|
||||
public void Round_trips_all_fields()
|
||||
{
|
||||
var entry = new HistorizationOutboxEntry(
|
||||
Guid.NewGuid(), "Pump1.Temp", 123.456, 192,
|
||||
Guid.NewGuid(), "Pump1.Temp", HistorianValue.FromDouble(123.456), 0u,
|
||||
new DateTime(2026, 1, 1, 12, 34, 56, DateTimeKind.Utc));
|
||||
|
||||
var bytes = HistorizationOutboxEntrySerializer.Serialize(entry);
|
||||
@@ -26,8 +26,8 @@ public sealed class HistorizationOutboxEntrySerializerTests
|
||||
|
||||
Assert.Equal(entry.Id, round.Id);
|
||||
Assert.Equal(entry.Tag, round.Tag);
|
||||
Assert.Equal(entry.NumericValue, round.NumericValue);
|
||||
Assert.Equal(entry.Quality, round.Quality);
|
||||
Assert.Equal(entry.Value, round.Value);
|
||||
Assert.Equal(entry.StatusCode, round.StatusCode);
|
||||
Assert.Equal(entry.TimestampUtc, round.TimestampUtc);
|
||||
Assert.Equal(entry.TimestampUtc.Kind, round.TimestampUtc.Kind); // ToBinary/FromBinary round-trips Kind
|
||||
}
|
||||
@@ -39,19 +39,19 @@ public sealed class HistorizationOutboxEntrySerializerTests
|
||||
// byte count diverge (e.g. Ω is 2 bytes, 日 is 3) is the one case that would desync the layout
|
||||
// if GetByteCount/GetBytes ever disagreed with what Deserialize slices out.
|
||||
var entry = new HistorizationOutboxEntry(
|
||||
Guid.NewGuid(), "Pumpe_Ω_日本語_Temp", -1.5, 0, DateTime.UtcNow);
|
||||
Guid.NewGuid(), "Pumpe_Ω_日本語_Temp", HistorianValue.FromDouble(-1.5), 0u, DateTime.UtcNow);
|
||||
|
||||
var round = HistorizationOutboxEntrySerializer.Deserialize(
|
||||
HistorizationOutboxEntrySerializer.Serialize(entry));
|
||||
|
||||
Assert.Equal(entry.Tag, round.Tag);
|
||||
Assert.Equal(entry.NumericValue, round.NumericValue);
|
||||
Assert.Equal(entry.Value, round.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trips_an_empty_tag()
|
||||
{
|
||||
var entry = new HistorizationOutboxEntry(Guid.NewGuid(), string.Empty, 0.0, 0, DateTime.UtcNow);
|
||||
var entry = new HistorizationOutboxEntry(Guid.NewGuid(), string.Empty, HistorianValue.FromDouble(0.0), 0u, DateTime.UtcNow);
|
||||
|
||||
var round = HistorizationOutboxEntrySerializer.Deserialize(
|
||||
HistorizationOutboxEntrySerializer.Serialize(entry));
|
||||
@@ -60,14 +60,13 @@ public sealed class HistorizationOutboxEntrySerializerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trips_boundary_quality_and_negative_value()
|
||||
public void Round_trips_boundary_negative_value()
|
||||
{
|
||||
var entry = new HistorizationOutboxEntry(Guid.NewGuid(), "T", double.MinValue, ushort.MaxValue, DateTime.UtcNow);
|
||||
var entry = new HistorizationOutboxEntry(Guid.NewGuid(), "T", HistorianValue.FromDouble(double.MinValue), 0u, DateTime.UtcNow);
|
||||
|
||||
var round = HistorizationOutboxEntrySerializer.Deserialize(
|
||||
HistorizationOutboxEntrySerializer.Serialize(entry));
|
||||
|
||||
Assert.Equal(ushort.MaxValue, round.Quality);
|
||||
Assert.Equal(double.MinValue, round.NumericValue);
|
||||
Assert.Equal(double.MinValue, round.Value.Number);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user