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; /// /// Tests for — the merge + diff logic for the /// transport entry plus per-platform probe entries that /// IHostConnectivityProbe.GetHostStatuses() surfaces. /// public sealed class HostStatusAggregatorTests { private static HostConnectivityStatus Status(string host, HostState state) => new(host, state, DateTime.UtcNow); [Fact] public void Snapshot_Empty_WhenNothingTracked() { var agg = new HostStatusAggregator(); agg.Snapshot().ShouldBeEmpty(); } [Fact] public void Update_NewHost_FiresChange_PreviousIsUnknown() { var agg = new HostStatusAggregator(); var captured = new List(); 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); } [Fact] public void Update_SameState_DoesNotFire() { var agg = new HostStatusAggregator(); agg.Update(Status("PlatformA", HostState.Running)); var captured = new List(); agg.OnHostStatusChanged += (_, e) => captured.Add(e); agg.Update(Status("PlatformA", HostState.Running)); captured.ShouldBeEmpty(); } [Fact] public void Update_StateTransition_FiresChangeWithCorrectPreviousAndNew() { var agg = new HostStatusAggregator(); agg.Update(Status("PlatformA", HostState.Running)); var captured = new List(); 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); } [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); } [Fact] public void Update_HostNameComparison_IsCaseInsensitive() { var agg = new HostStatusAggregator(); var captured = new List(); 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); } [Fact] public void Remove_TrackedHost_ReturnsTrue_AndDropsFromSnapshot() { var agg = new HostStatusAggregator(); agg.Update(Status("PlatformA", HostState.Running)); agg.Remove("PlatformA").ShouldBeTrue(); agg.Snapshot().ShouldBeEmpty(); } [Fact] public void Remove_UnknownHost_ReturnsFalse() { var agg = new HostStatusAggregator(); agg.Remove("Nope").ShouldBeFalse(); } [Fact] public void 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)); } })).ToArray(); Task.WaitAll(tasks); agg.Snapshot().Count.ShouldBeLessThanOrEqualTo(32); } }