Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AlarmsHistorianReaderTests.cs
T
Joseph Doherty b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via
VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask,
SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass
TestContext.Current.CancellationToken. Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 08:57:33 -05:00

101 lines
4.3 KiB
C#

using Akka.Actor;
using Akka.Hosting;
using Akka.TestKit.Xunit;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Runtime;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
/// <summary>
/// R04 L3: <c>/alarms-historian</c> distinguishes "this role runs no historian" (a permanent state, shown
/// once, not polled) from a sink that did not answer, instead of swallowing both into "Loading…" forever.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AlarmsHistorianReaderTests : TestKit
{
private static readonly HistorianSinkStatus Status = new(3, 1, null, null, null, HistorianDrainState.Idle);
private sealed class StatusActor : ReceiveActor
{
public StatusActor() => Receive<HistorianAdapterActor.GetStatus>(_ => Sender.Tell(Status));
}
[Fact]
public async Task An_answering_sink_is_available()
{
var actor = Sys.ActorOf(Props.Create(() => new StatusActor()));
var reading = await HistorianSinkStatusReader.ReadAsync(() => actor, TimeSpan.FromSeconds(5), CancellationToken.None);
reading.Availability.ShouldBe(HistorianSinkAvailability.Available);
reading.Status.ShouldBe(Status);
}
[Fact]
public async Task A_node_without_the_registry_entry_reports_not_on_this_role()
{
// The real Akka.Hosting registry: an admin-only node never registers the historian key.
var registry = new ActorRegistry();
var reading = await HistorianSinkStatusReader.ReadAsync(
() => registry.Get<HistorianAdapterActorKey>(), TimeSpan.FromSeconds(5), CancellationToken.None);
reading.Availability.ShouldBe(HistorianSinkAvailability.NotOnThisRole);
reading.Status.ShouldBeNull();
reading.Message!.ShouldContain("driver role");
}
[Fact]
public async Task A_silent_sink_is_unavailable_not_loading()
{
var silent = CreateTestProbe(); // receives the Ask, never replies
var reading = await HistorianSinkStatusReader.ReadAsync(() => silent.Ref, TimeSpan.FromMilliseconds(200), CancellationToken.None);
reading.Availability.ShouldBe(HistorianSinkAvailability.Unavailable);
reading.Message!.ShouldContain("did not answer");
// Positive control: the Ask really was delivered — the classification is about the missing reply.
silent.ExpectMsg<HistorianAdapterActor.GetStatus>(cancellationToken: TestContext.Current.CancellationToken);
}
[Fact]
public async Task Page_teardown_cancellation_propagates()
{
var silent = CreateTestProbe();
using var cts = new CancellationTokenSource();
cts.Cancel();
await Should.ThrowAsync<OperationCanceledException>(() =>
HistorianSinkStatusReader.ReadAsync(() => silent.Ref, TimeSpan.FromSeconds(30), cts.Token));
}
[Fact]
public void Not_registered_keeps_a_slow_repoll_instead_of_stopping()
{
// Review r0 M6: a driver node opened mid-startup is "not registered" for a moment — never permanent.
HistorianSinkStatusReader.PollIntervalFor(HistorianSinkAvailability.NotOnThisRole)
.ShouldBe(HistorianSinkStatusReader.NotRegisteredPollInterval);
HistorianSinkStatusReader.PollIntervalFor(HistorianSinkAvailability.Available)
.ShouldBe(HistorianSinkStatusReader.PollInterval);
HistorianSinkStatusReader.PollIntervalFor(null).ShouldBe(HistorianSinkStatusReader.PollInterval);
var src = AdminUiSource.Read("Components", "Pages", "AlarmsHistorian.razor");
src.ShouldContain("_timer.Change(interval, interval)");
src.ShouldNotContain("if (_reading?.Availability == HistorianSinkAvailability.NotOnThisRole) return;");
}
[Fact]
public void Page_uses_the_reader_and_the_async_dispose_lifecycle()
{
var src = AdminUiSource.Read("Components", "Pages", "AlarmsHistorian.razor");
src.ShouldContain("HistorianSinkStatusReader.ReadAsync(");
src.ShouldContain("@implements IAsyncDisposable");
src.ShouldContain("HistorianSinkAvailability.NotOnThisRole");
System.Text.RegularExpressions.Regex.IsMatch(src, @"catch\s*\{").ShouldBeFalse("a bare catch {} swallows every failure");
}
}