diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/ScanStateProbeParityTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/ScanStateProbeParityTests.cs new file mode 100644 index 0000000..9efad27 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/ScanStateProbeParityTests.cs @@ -0,0 +1,100 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests; + +/// +/// PR 5.8 — Per-platform ScanState probe parity. The legacy backend's +/// GalaxyRuntimeProbeManager and the in-process backend's +/// PerPlatformProbeWatcher (PR 4.7) must surface the same per-host +/// stream after Discover: same host name +/// set, matching per host. +/// +[Trait("Category", "ParityE2E")] +[Collection(nameof(ParityCollection))] +public sealed class ScanStateProbeParityTests +{ + private readonly ParityHarness _h; + public ScanStateProbeParityTests(ParityHarness h) => _h = h; + + [Fact] + public async Task GetHostStatuses_emits_same_host_set_after_Discover() + { + _h.RequireBoth(); + + // Probe-watcher membership only refreshes after a Discover pass — drive that + // first so both backends have populated their per-platform tracker. + var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) => + { + var b = new RecordingAddressSpaceBuilder(); + await ((ITagDiscovery)driver).DiscoverAsync(b, ct); + // Give the probe watcher a beat to land its initial ScanState reads — + // PR 4.7 subscribes per platform with bufferedUpdateIntervalMs=0 so the + // first push lands within ~publishingInterval (1s default). + await Task.Delay(1_500, ct); + return ((IHostConnectivityProbe)driver).GetHostStatuses(); + }, CancellationToken.None); + + var legacy = snapshots[ParityHarness.Backend.LegacyHost]; + var mxgw = snapshots[ParityHarness.Backend.MxGateway]; + + // Legacy reports: client-name transport entry + every $WinPlatform/$AppEngine + // probe. Mxgw reports the same shape (PR 4.7). The host-name set must agree + // case-insensitively. + var legacyHosts = legacy.Select(s => s.HostName).ToHashSet(StringComparer.OrdinalIgnoreCase); + var mxgwHosts = mxgw.Select(s => s.HostName).ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (legacyHosts.Count == 0) + { + Assert.Skip("legacy backend reported no host probes — dev Galaxy may not be a multi-platform deployment"); + } + + // The transport-entry host names differ by design — legacy uses the legacy + // host's process-level identity, mxgw uses MxAccess.ClientName. Compare + // only the platform-host subset (anything that's NOT either side's transport). + var legacyPlatformHosts = legacyHosts.Where(h => !h.Contains("Galaxy.Host", StringComparison.OrdinalIgnoreCase)).ToHashSet(StringComparer.OrdinalIgnoreCase); + var mxgwPlatformHosts = mxgwHosts.Where(h => !h.Contains("OtOpcUa-Parity", StringComparison.OrdinalIgnoreCase)).ToHashSet(StringComparer.OrdinalIgnoreCase); + + legacyPlatformHosts.Except(mxgwPlatformHosts, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty( + "every $WinPlatform / $AppEngine probed by the legacy backend must appear in the mxgw probe set"); + mxgwPlatformHosts.Except(legacyPlatformHosts, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty( + "every $WinPlatform / $AppEngine probed by the mxgw backend must appear in the legacy probe set"); + } + + [Fact] + public async Task GetHostStatuses_state_per_platform_matches_across_backends() + { + _h.RequireBoth(); + + var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) => + { + var b = new RecordingAddressSpaceBuilder(); + await ((ITagDiscovery)driver).DiscoverAsync(b, ct); + await Task.Delay(1_500, ct); + return ((IHostConnectivityProbe)driver).GetHostStatuses() + .ToDictionary(s => s.HostName, s => s.State, StringComparer.OrdinalIgnoreCase); + }, CancellationToken.None); + + var legacy = snapshots[ParityHarness.Backend.LegacyHost]; + var mxgw = snapshots[ParityHarness.Backend.MxGateway]; + + if (legacy.Count == 0 || mxgw.Count == 0) + { + Assert.Skip("one or both backends reported no host probes"); + } + + // Skip the transport entry per backend (different by design); compare the + // platform-host overlap. + var commonHosts = legacy.Keys.Intersect(mxgw.Keys, StringComparer.OrdinalIgnoreCase).ToArray(); + if (commonHosts.Length == 0) + { + Assert.Skip("no overlapping platform hosts between backends — likely the transport names differ but no $WinPlatform was discovered"); + } + + foreach (var host in commonHosts) + { + mxgw[host].ShouldBe(legacy[host], $"HostState parity for '{host}'"); + } + } +}