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}'"); } } }