Files
Joseph Doherty 0b85c499e3 fix(historian): refuse native Gateway TimeAverage/Total until historiangw#27 (lmxopcua#746)
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
2026-11-09 15:40:46 -05:00

65 lines
3.6 KiB
C#

using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions; // HistoryAggregateType
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; // RetrievalMode
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
public sealed class AggregateModeMapperTests
{
[Theory]
[InlineData(HistoryAggregateType.TimeAverage, RetrievalMode.TimeWeightedAverage)]
[InlineData(HistoryAggregateType.Total, RetrievalMode.Integral)]
public void Maps_each_aggregate(HistoryAggregateType a, RetrievalMode expected)
=> Assert.Equal(expected, AggregateModeMapper.ToRetrievalMode(a));
[Fact] // matrix guard: every member is either mapped or explicitly reported as not supported
public void Every_aggregate_member_is_mapped_or_reports_not_supported()
{
var original = new[] { HistoryAggregateType.Total, HistoryAggregateType.TimeAverage };
Assert.Equal(original.OrderBy(a => a), AggregateModeMapper.NativeAggregates.OrderBy(a => a));
foreach (var a in Enum.GetValues<HistoryAggregateType>())
{
if (original.Contains(a))
_ = AggregateModeMapper.ToRetrievalMode(a);
else
Assert.Throws<HistoryAggregateNotSupportedException>(() => AggregateModeMapper.ToRetrievalMode(a));
}
}
[Theory] // #679: AVEVA has no arithmetic mean and no sample count; #681: the per-sample aggregates are raw-evaluated, not native
[InlineData(HistoryAggregateType.Average)]
[InlineData(HistoryAggregateType.Count)]
[InlineData(HistoryAggregateType.Minimum)]
[InlineData(HistoryAggregateType.Maximum)]
public void Per_sample_aggregates_have_no_native_mode(HistoryAggregateType a)
=> Assert.Throws<HistoryAggregateNotSupportedException>(() => AggregateModeMapper.ToRetrievalMode(a));
[Fact] // #682/#681: the served (= advertised) set is the native members plus the raw-evaluated per-sample families,
// and the data source returns exactly that static
public void Served_is_exactly_the_mapped_members_and_the_data_source_declares_it()
{
foreach (var a in Enum.GetValues<HistoryAggregateType>())
{
var native = AggregateModeMapper.NativeAggregates.Contains(a);
var perSample = ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.AggregateFamilies.FamilyOf(a) is
ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.AggregateFamily.Simple
or ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.AggregateFamily.StartEnd
or ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.AggregateFamily.ActualTime;
Assert.Equal(native || perSample, GatewayHistorianDataSource.ServedAggregates.Contains(a));
if (!native) Assert.Throws<HistoryAggregateNotSupportedException>(() => AggregateModeMapper.ToRetrievalMode(a));
}
Assert.Equal(12, GatewayHistorianDataSource.ServedAggregates.Count);
// The native pair is opt-in (ServerHistorian:Gateway:EnableNativeTimeWeighted) — see GatewayNativeTimeWeightedGateTests.
var source = new GatewayHistorianDataSource(
new FakeHistorianGatewayClient(), Microsoft.Extensions.Logging.Abstractions.NullLogger<GatewayHistorianDataSource>.Instance,
enableNativeTimeWeighted: true);
Assert.Same(GatewayHistorianDataSource.ServedAggregates, source.SupportedAggregates);
}
[Fact]
public void Undefined_value_is_a_programming_error_not_a_not_supported()
=> Assert.Throws<ArgumentOutOfRangeException>(() => AggregateModeMapper.ToRetrievalMode((HistoryAggregateType)9999));
}