Merge ph-T4 (pluggable historian)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 12:57:52 -04:00
6 changed files with 198 additions and 5 deletions
@@ -0,0 +1,38 @@
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
public sealed class GatewayHistorianProviderTests
{
private static GatewayHistorianProvider Provider() =>
new(new ServerHistorianOptions { Endpoint = "http://localhost:5222", UseTls = false, ApiKey = "histgw_test_secret" });
private static IServiceProvider Sp() => new ServiceCollection().BuildServiceProvider();
[Fact]
public void Shape_is_gateway_numeric_without_updater()
{
var p = Provider();
Assert.Equal("Gateway", p.Name);
Assert.Equal(
new[] { HistorianValueKind.Double, HistorianValueKind.Int64, HistorianValueKind.Boolean }.OrderBy(x => x),
p.SupportedKinds.OrderBy(x => x));
Assert.Null(p.CreateUpdater(Sp()));
}
[Fact]
public async Task Create_methods_build_gateway_seams()
{
var p = Provider();
var sp = Sp();
Assert.IsType<GatewayHistorianDataSource>(p.CreateDataSource(sp));
var writer = Assert.IsType<GatewayHistorianValueWriter>(p.CreateValueWriter(sp));
Assert.IsType<GatewayAlarmHistorianWriter>(p.CreateAlarmWriter(sp));
Assert.IsType<GatewayTagProvisioner>(p.CreateProvisioning(sp));
await writer.DisposeAsync();
}
}
@@ -0,0 +1,22 @@
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
public sealed class GatewayQualityMapperReverseTests
{
[Theory]
[InlineData((byte)192)] [InlineData((byte)216)]
[InlineData((byte)64)] [InlineData((byte)68)] [InlineData((byte)80)] [InlineData((byte)84)] [InlineData((byte)88)]
[InlineData((byte)0)] [InlineData((byte)4)] [InlineData((byte)8)] [InlineData((byte)12)]
[InlineData((byte)16)] [InlineData((byte)20)] [InlineData((byte)24)] [InlineData((byte)32)]
public void Round_trips_every_explicit_byte(byte q)
=> Assert.Equal(q, GatewayQualityMapper.ToDaQuality(GatewayQualityMapper.Map(q)));
[Theory]
[InlineData(0x00AB0000u, (byte)192)]
[InlineData(0x40AB0000u, (byte)64)]
[InlineData(0x80AB0000u, (byte)0)]
public void Unknown_status_falls_back_by_severity(uint status, byte expected)
=> Assert.Equal(expected, GatewayQualityMapper.ToDaQuality(status));
}
@@ -146,4 +146,33 @@ public sealed class GatewayHistorianValueWriterTests
Assert.Equal(1, fake.DisposeCallCount);
}
[Fact]
public async Task Bad_status_maps_to_da_quality()
{
var fake = new FakeHistorianGatewayClient();
var values = new[] { new HistorizationValue(null, HistorianValue.FromDouble(1.0), 0x808A0000u) };
await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
Assert.Equal(8u, fake.LastWriteLiveValues![0].Quality);
}
[Fact]
public async Task Non_numeric_value_is_skipped_without_throwing()
{
var fake = new FakeHistorianGatewayClient();
var values = new[]
{
new HistorizationValue(null, HistorianValue.FromString("x"), 0u),
new HistorizationValue(null, HistorianValue.FromDouble(42.5), 0u),
};
var ok = await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken);
Assert.True(ok);
var sent = Assert.Single(fake.LastWriteLiveValues!);
Assert.Equal(42.5, sent.NumericValue);
Assert.Equal(192u, sent.Quality);
}
}