b8202e1147
v2-ci / unit-tests (push) Has been cancelled
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
147 lines
5.4 KiB
C#
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);
|
|
}
|
|
}
|