Files
lmxopcua/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/HistoryReadAggregateParsingTests.cs
T
Joseph Doherty a74a18e49a fix(client): offer exactly the served aggregates in the CLI and UI
R08 2.3: AggregateType drops StandardDeviation (no backend serves it; always
BadAggregateNotSupported) and gains Range, Delta, MinimumActualTime,
MaximumActualTime, PercentGood and PercentBad. A parity test pins the enum to
AggregateFamilies.Served and each member to its same-named Part 13 ObjectId.
The CLI parses every enum name (plus aliases) and the UI picker lists them all.
Existing members keep their numeric values; 6 is retired.

Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 16:27:08 -04:00

88 lines
3.4 KiB
C#

using CliFx.Exceptions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
/// <summary>
/// <c>historyread --aggregate</c> accepts every aggregate the client offers (which is exactly the served set), so the
/// gate recipes in docs/Historian.md that need Range / Delta / PercentGood / ... can run from this CLI.
/// </summary>
public class HistoryReadAggregateParsingTests
{
/// <summary>Every client aggregate, by its name.</summary>
/// <returns>Each <see cref="AggregateType"/> value.</returns>
public static TheoryData<AggregateType> AllAggregates() => new(Enum.GetValues<AggregateType>());
/// <summary>Every aggregate name (in any case) reaches the service as that aggregate.</summary>
/// <param name="aggregate">The aggregate.</param>
[Theory]
[MemberData(nameof(AllAggregates))]
public async Task Every_aggregate_name_parses(AggregateType aggregate)
{
var fakeService = new FakeOpcUaClientService();
var command = new HistoryReadCommand(new FakeOpcUaClientServiceFactory(fakeService))
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=HistNode",
Aggregate = aggregate.ToString().ToUpperInvariant()
};
using var console = TestConsoleHelper.CreateConsole();
await command.ExecuteAsync(console);
fakeService.HistoryReadAggregateCalls.ShouldHaveSingleItem().Aggregate.ShouldBe(aggregate);
}
/// <summary>The short aliases still parse.</summary>
/// <param name="alias">The alias.</param>
/// <param name="expected">The aggregate it names.</param>
[Theory]
[InlineData("avg", AggregateType.Average)]
[InlineData("min", AggregateType.Minimum)]
[InlineData("last", AggregateType.End)]
[InlineData("interp", AggregateType.Interpolative)]
[InlineData("mintime", AggregateType.MinimumActualTime)]
[InlineData("maxtime", AggregateType.MaximumActualTime)]
public async Task Aliases_parse(string alias, AggregateType expected)
{
var fakeService = new FakeOpcUaClientService();
var command = new HistoryReadCommand(new FakeOpcUaClientServiceFactory(fakeService))
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=HistNode",
Aggregate = alias
};
using var console = TestConsoleHelper.CreateConsole();
await command.ExecuteAsync(console);
fakeService.HistoryReadAggregateCalls.ShouldHaveSingleItem().Aggregate.ShouldBe(expected);
}
/// <summary>
/// StandardDeviation is no longer offered (no backend serves it): it is refused before connecting, naming the
/// supported set.
/// </summary>
[Fact]
public async Task StandardDeviation_is_refused_before_connecting()
{
var fakeService = new FakeOpcUaClientService();
var command = new HistoryReadCommand(new FakeOpcUaClientServiceFactory(fakeService))
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=HistNode",
Aggregate = "stddev"
};
using var console = TestConsoleHelper.CreateConsole();
var ex = await Should.ThrowAsync<CommandException>(async () => await command.ExecuteAsync(console));
ex.Message.ShouldContain("PercentGood");
fakeService.HistoryReadAggregateCalls.ShouldBeEmpty();
}
}