Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayContinuousHistorizationWarningTests.cs
T
Joseph Doherty 096a3d5b3c docs(historian): backend capability matrix; warn on Gateway continuous historization (lmxopcua#746)
The Gateway backend's gaps against SqlServer/QuestDb were documented
piecemeal. The backend-selection table in docs/Historian.md now also covers
the aggregate set (and the native time-weighted opt-in), emulated reverse
and end-only reads, the client-side events cap, retention and the health
probe.

Continuous historization on Gateway silently loses sample quality (stored
Good), comms-loss outage markers and non-numeric values. Building its live
writer, which only happens when ContinuousHistorization is enabled, now
logs one startup Warning that names the gaps and the alternatives.

The review asked for the matrix in the AdminUI backend picker, but no such
picker exists: the backend is configuration-only. Filed as a follow-up.

Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22
in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:40:47 -05:00

58 lines
2.7 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
/// <summary>
/// Continuous historization on the Gateway backend silently loses what the SQL backends keep — every sample's
/// quality (the gateway stores Good), comms-loss outage markers and non-numeric values — so building the live writer
/// (which only happens when continuous historization is enabled) logs one startup Warning naming the gaps.
/// </summary>
public sealed class GatewayContinuousHistorizationWarningTests
{
private sealed class CapturingProvider : ILoggerProvider
{
public readonly List<(LogLevel Level, string Message)> Entries = new();
public ILogger CreateLogger(string categoryName) => new L(this);
public void Dispose() { }
private sealed class L(CapturingProvider owner) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
lock (owner.Entries) owner.Entries.Add((logLevel, formatter(state, exception)));
}
}
}
[Fact]
public async Task Building_the_gateway_live_writer_warns_once_about_what_the_backend_drops()
{
var capture = new CapturingProvider();
using var sp = new ServiceCollection().AddLogging(b => b.AddProvider(capture)).BuildServiceProvider();
var provider = new GatewayHistorianProvider(
new ServerHistorianOptions { Endpoint = "http://localhost:5222", UseTls = false, ApiKey = "histgw_test_secret" });
var writer = Assert.IsType<GatewayHistorianValueWriter>(provider.CreateValueWriter(sp));
var warning = capture.Entries.Where(e => e.Level == LogLevel.Warning).ShouldHaveSingleItem().Message;
warning.ShouldContain("quality");
warning.ShouldContain("Good");
warning.ShouldContain("outage");
warning.ShouldContain("non-numeric");
warning.ShouldContain("SqlServer");
await writer.DisposeAsync();
// Positive control in the other direction: the read path alone (no continuous historization) does not warn.
capture.Entries.Clear();
await using var ds = (GatewayHistorianDataSource)provider.CreateDataSource(sp);
capture.Entries.ShouldNotContain(e => e.Level == LogLevel.Warning);
}
}