PR 5.8 — Per-platform ScanState probe parity scenarios
Closes Phase 5 scenario coverage. Both GalaxyRuntimeProbeManager (legacy) and PerPlatformProbeWatcher (PR 4.7) must surface the same per-host status stream: - GetHostStatuses_emits_same_host_set_after_Discover — drives Discover on both backends, waits 1.5s for the probe watcher's first push, then asserts the platform-host set agrees (transport-entry names differ by design — legacy uses the Galaxy.Host process identity, mxgw uses MxAccess.ClientName, so we strip those before comparing). - GetHostStatuses_state_per_platform_matches_across_backends — for every overlapping platform host, the HostState must be identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.8 — Per-platform <c>ScanState</c> probe parity. The legacy backend's
|
||||
/// <c>GalaxyRuntimeProbeManager</c> and the in-process backend's
|
||||
/// <c>PerPlatformProbeWatcher</c> (PR 4.7) must surface the same per-host
|
||||
/// <see cref="HostConnectivityStatus"/> stream after Discover: same host name
|
||||
/// set, matching <see cref="HostState"/> per host.
|
||||
/// </summary>
|
||||
[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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user