Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
4.4 KiB
C#
138 lines
4.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);
|
|
|
|
[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<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);
|
|
}
|
|
|
|
[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();
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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<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);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|