0b85c499e3
AVEVA's native TimeAverage / Total are answered with Good-status wrong values while the gateway sends the interval in the wrong unit (historiangw#27): one whole-span value on the earliest interval, the rest absent. Nothing on the wire marks the answer as degraded. Both are now refused BadAggregateNotSupported (unsent, not counted as a backend failure, not advertised in AggregateFunctions) unless ServerHistorian:Gateway:EnableNativeTimeWeighted is true. The default Gateway set is the ten raw-evaluated aggregates (GatewayHistorianDataSource.RawEvaluatedAggregates); ServedAggregates stays the 12 the backend can serve with the flag on. The native-path tests opt in. Also documents the ServerHistorian:Gateway sub-section (EventDrainBudget from the previous commit) and the events drain in docs/Historian.md. 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
86 lines
4.3 KiB
C#
86 lines
4.3 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
|
|
|
|
/// <summary>
|
|
/// AVEVA's native TimeAverage / Total answer Good-status wrong values until the gateway's interval fix
|
|
/// (historiangw#27) is live-verified, so they are refused <c>BadAggregateNotSupported</c> unless
|
|
/// <c>ServerHistorian:Gateway:EnableNativeTimeWeighted</c> opts back in.
|
|
/// </summary>
|
|
public sealed class GatewayNativeTimeWeightedGateTests
|
|
{
|
|
private static readonly DateTime T0 = new(2026, 10, 22, 0, 0, 0, DateTimeKind.Utc);
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
[Theory]
|
|
[InlineData(HistoryAggregateType.TimeAverage)]
|
|
[InlineData(HistoryAggregateType.Total)]
|
|
public async Task By_default_the_native_time_weighted_pair_is_refused_unsent_and_unadvertised(HistoryAggregateType a)
|
|
{
|
|
var fake = new FakeHistorianGatewayClient();
|
|
var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
|
|
|
|
ds.SupportedAggregates.ShouldNotContain(a);
|
|
await Should.ThrowAsync<HistoryAggregateNotSupportedException>(
|
|
() => ds.ReadProcessedAsync("T", T0, T0.AddHours(1), TimeSpan.FromMinutes(1), a, Ct));
|
|
fake.ReadAggregateCallCount.ShouldBe(0);
|
|
fake.TagExistsCallCount.ShouldBe(0);
|
|
ds.GetHealthSnapshot().TotalQueries.ShouldBe(0, "a refused aggregate is a client request, not a backend read");
|
|
|
|
// Positive control: a raw-evaluated per-sample aggregate on the same source is still served.
|
|
ds.SupportedAggregates.ShouldContain(HistoryAggregateType.Average);
|
|
var avg = await ds.ReadProcessedAsync("T", T0, T0.AddHours(1), TimeSpan.FromMinutes(30), HistoryAggregateType.Average, Ct);
|
|
avg.Samples.Count.ShouldBe(2);
|
|
fake.ReadRawCallCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(HistoryAggregateType.TimeAverage)]
|
|
[InlineData(HistoryAggregateType.Total)]
|
|
public async Task Opting_in_serves_and_advertises_the_native_pair(HistoryAggregateType a)
|
|
{
|
|
var fake = new FakeHistorianGatewayClient();
|
|
var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance, enableNativeTimeWeighted: true);
|
|
|
|
ds.SupportedAggregates.ShouldBe(GatewayHistorianDataSource.ServedAggregates);
|
|
await ds.ReadProcessedAsync("T", T0, T0.AddHours(1), TimeSpan.FromMinutes(1), a, Ct);
|
|
fake.ReadAggregateCallCount.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_default_set_is_the_ten_raw_evaluated_aggregates()
|
|
{
|
|
var ds = new GatewayHistorianDataSource(new FakeHistorianGatewayClient(), NullLogger<GatewayHistorianDataSource>.Instance);
|
|
ds.SupportedAggregates.Count.ShouldBe(10);
|
|
ds.SupportedAggregates.ShouldBe(GatewayHistorianDataSource.RawEvaluatedAggregates);
|
|
GatewayHistorianDataSource.ServedAggregates.Except(ds.SupportedAggregates).OrderBy(x => x)
|
|
.ShouldBe([HistoryAggregateType.Total, HistoryAggregateType.TimeAverage]);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null, false)]
|
|
[InlineData("false", false)]
|
|
[InlineData("true", true)]
|
|
public void The_flag_binds_from_configuration_and_reaches_the_data_source(string? configured, bool expected)
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ServerHistorian:Enabled"] = "true",
|
|
["ServerHistorian:Endpoint"] = "https://localhost:5222",
|
|
["ServerHistorian:ApiKey"] = "histgw_x_y",
|
|
};
|
|
if (configured is not null) values["ServerHistorian:Gateway:EnableNativeTimeWeighted"] = configured;
|
|
var opts = new ConfigurationBuilder().AddInMemoryCollection(values).Build()
|
|
.GetSection(ServerHistorianOptions.SectionName).Get<ServerHistorianOptions>()!;
|
|
using var services = new ServiceCollection().BuildServiceProvider();
|
|
var ds = Assert.IsType<GatewayHistorianDataSource>(GatewayHistorian.CreateDataSource(opts, services));
|
|
ds.SupportedAggregates.Contains(HistoryAggregateType.TimeAverage).ShouldBe(expected);
|
|
}
|
|
}
|