Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Health/HostStatusAggregatorTests.cs
T
Joseph Doherty b8202e1147
v2-ci / unit-tests (push) Has been cancelled
test: remaining xUnit1051/1031/1030, CS8602/8604, CS0618, CS0219, CS0649, NU1510 fixes (lmxopcua#725)
Awaited deadline oracles get the runner token; sync-over-async tests made async;
nullable child models asserted via ShouldNotBeNull() return values; obsolete SDK
ctors replaced with the telemetry overloads; dead locals/fixture and the redundant
DI PackageReference removed. No suppressions.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 13:22:34 -05:00

147 lines
5.4 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Health;
/// <summary>
/// Tests for <see cref="HostStatusAggregator"/> — the merge + diff logic for the
/// transport entry plus per-platform probe entries that
/// <c>IHostConnectivityProbe.GetHostStatuses()</c> surfaces.
/// </summary>
public sealed class HostStatusAggregatorTests
{
private static HostConnectivityStatus Status(string host, HostState state) =>
new(host, state, DateTime.UtcNow);
/// <summary>Verifies that snapshot is empty when nothing is tracked.</summary>
[Fact]
public void Snapshot_Empty_WhenNothingTracked()
{
var agg = new HostStatusAggregator();
agg.Snapshot().ShouldBeEmpty();
}
/// <summary>Verifies that updating a new host fires a change event with Unknown as previous state.</summary>
[Fact]
public void Update_NewHost_FiresChange_PreviousIsUnknown()
{
var agg = new HostStatusAggregator();
var captured = new List<HostStatusChangedEventArgs>();
agg.OnHostStatusChanged += (_, e) => captured.Add(e);
agg.Update(Status("PlatformA", HostState.Running));
captured.Count.ShouldBe(1);
captured[0].HostName.ShouldBe("PlatformA");
captured[0].OldState.ShouldBe(HostState.Unknown);
captured[0].NewState.ShouldBe(HostState.Running);
}
/// <summary>Verifies that updating to the same state does not fire a change event.</summary>
[Fact]
public void Update_SameState_DoesNotFire()
{
var agg = new HostStatusAggregator();
agg.Update(Status("PlatformA", HostState.Running));
var captured = new List<HostStatusChangedEventArgs>();
agg.OnHostStatusChanged += (_, e) => captured.Add(e);
agg.Update(Status("PlatformA", HostState.Running));
captured.ShouldBeEmpty();
}
/// <summary>Verifies that state transitions fire change events with correct old and new states.</summary>
[Fact]
public void Update_StateTransition_FiresChangeWithCorrectPreviousAndNew()
{
var agg = new HostStatusAggregator();
agg.Update(Status("PlatformA", HostState.Running));
var captured = new List<HostStatusChangedEventArgs>();
agg.OnHostStatusChanged += (_, e) => captured.Add(e);
agg.Update(Status("PlatformA", HostState.Stopped));
captured.Count.ShouldBe(1);
captured[0].OldState.ShouldBe(HostState.Running);
captured[0].NewState.ShouldBe(HostState.Stopped);
}
/// <summary>Verifies that snapshot reflects every upserted host.</summary>
[Fact]
public void Snapshot_ReflectsEveryUpsertedHost()
{
var agg = new HostStatusAggregator();
agg.Update(Status("Transport", HostState.Running));
agg.Update(Status("PlatformA", HostState.Running));
agg.Update(Status("PlatformB", HostState.Stopped));
var snap = agg.Snapshot();
snap.Count.ShouldBe(3);
snap.Select(s => s.HostName).OrderBy(x => x).ShouldBe(new[] { "PlatformA", "PlatformB", "Transport" });
snap.First(s => s.HostName == "PlatformB").State.ShouldBe(HostState.Stopped);
}
/// <summary>Verifies that host name comparison is case-insensitive.</summary>
[Fact]
public void Update_HostNameComparison_IsCaseInsensitive()
{
var agg = new HostStatusAggregator();
var captured = new List<HostStatusChangedEventArgs>();
agg.OnHostStatusChanged += (_, e) => captured.Add(e);
agg.Update(Status("PlatformA", HostState.Running));
agg.Update(Status("platforma", HostState.Stopped)); // same host, different case
captured.Count.ShouldBe(2);
captured[1].OldState.ShouldBe(HostState.Running);
captured[1].NewState.ShouldBe(HostState.Stopped);
agg.Snapshot().Count.ShouldBe(1);
}
/// <summary>Verifies that removing a tracked host returns true and drops it from snapshot.</summary>
[Fact]
public void Remove_TrackedHost_ReturnsTrue_AndDropsFromSnapshot()
{
var agg = new HostStatusAggregator();
agg.Update(Status("PlatformA", HostState.Running));
agg.Remove("PlatformA").ShouldBeTrue();
agg.Snapshot().ShouldBeEmpty();
}
/// <summary>Verifies that removing an unknown host returns false.</summary>
[Fact]
public void Remove_UnknownHost_ReturnsFalse()
{
var agg = new HostStatusAggregator();
agg.Remove("Nope").ShouldBeFalse();
}
/// <summary>Verifies that concurrent updates do not corrupt the internal dictionary.</summary>
[Fact]
public async Task ConcurrentUpdates_DoNotCorruptDictionary()
{
var agg = new HostStatusAggregator();
const int threadCount = 8;
const int updatesPerThread = 250;
var tasks = Enumerable.Range(0, threadCount).Select(t => Task.Run(() =>
{
for (var i = 0; i < updatesPerThread; i++)
{
var hostName = $"Host{(t * updatesPerThread + i) % 32}";
var state = i % 2 == 0 ? HostState.Running : HostState.Stopped;
agg.Update(Status(hostName, state));
}
}, TestContext.Current.CancellationToken)).ToArray();
await Task.WhenAll(tasks).WaitAsync(TestContext.Current.CancellationToken);
agg.Snapshot().Count.ShouldBeLessThanOrEqualTo(32);
}
}