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; /// /// 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 BadAggregateNotSupported unless /// ServerHistorian:Gateway:EnableNativeTimeWeighted opts back in. /// 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.Instance); ds.SupportedAggregates.ShouldNotContain(a); await Should.ThrowAsync( () => 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.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.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 { ["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()!; using var services = new ServiceCollection().BuildServiceProvider(); var ds = Assert.IsType(GatewayHistorian.CreateDataSource(opts, services)); ds.SupportedAggregates.Contains(HistoryAggregateType.TimeAverage).ShouldBe(expected); } }