PR 5.6 — History-read parity scenarios
Galaxy history reads route through the server-owned HistoryRouter (Phase 1, PR 1.3) — neither Galaxy backend implements IHistoryProvider directly. Parity surface here is the routing decision: - Discover_emits_same_historized_attribute_set_for_both_backends — the IsHistorized attribute set must agree symmetric-set-wise; that's what HistoryRouter consumes when deciding whether to route a HistoryRead to the Wonderware historian sidecar. - Neither_Galaxy_backend_implements_IHistoryProvider_directly — pins the architectural decision so a regression that re-introduces a per-driver history path fires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.6 — History-read parity. Phase-1 routing lifted history off the
|
||||
/// per-driver <see cref="IHistoryProvider"/> path onto the server-owned
|
||||
/// <c>HistoryRouter</c> + <c>WonderwareHistorianBootstrap</c>; neither
|
||||
/// Galaxy backend implements <see cref="IHistoryProvider"/> directly. So
|
||||
/// the parity surface here is the *routing decision*: both backends must
|
||||
/// identify the same set of historized attributes and produce the same
|
||||
/// full-reference for each, so HistoryRouter routes reads identically.
|
||||
/// </summary>
|
||||
[Trait("Category", "ParityE2E")]
|
||||
[Collection(nameof(ParityCollection))]
|
||||
public sealed class HistoryReadParityTests
|
||||
{
|
||||
private readonly ParityHarness _h;
|
||||
public HistoryReadParityTests(ParityHarness h) => _h = h;
|
||||
|
||||
[Fact]
|
||||
public async Task Discover_emits_same_historized_attribute_set_for_both_backends()
|
||||
{
|
||||
_h.RequireBoth();
|
||||
|
||||
var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) =>
|
||||
{
|
||||
var b = new RecordingAddressSpaceBuilder();
|
||||
await ((ITagDiscovery)driver).DiscoverAsync(b, ct);
|
||||
return b.Variables
|
||||
.Where(v => v.AttributeInfo.IsHistorized)
|
||||
.Select(v => v.AttributeInfo.FullName)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}, CancellationToken.None);
|
||||
|
||||
var legacy = snapshots[ParityHarness.Backend.LegacyHost];
|
||||
var mxgw = snapshots[ParityHarness.Backend.MxGateway];
|
||||
|
||||
if (legacy.Count == 0)
|
||||
{
|
||||
Assert.Skip("dev Galaxy has no historized attributes — history routing parity unverified for this rig");
|
||||
}
|
||||
|
||||
legacy.Except(mxgw, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty(
|
||||
"every historized attribute discovered by the legacy backend must appear in the mxgw backend");
|
||||
mxgw.Except(legacy, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty(
|
||||
"every historized attribute discovered by the mxgw backend must appear in the legacy backend");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Neither_Galaxy_backend_implements_IHistoryProvider_directly()
|
||||
{
|
||||
// Pinning the architectural decision from Phase 1 (PR 1.3): per-driver
|
||||
// IHistoryProvider was retired in favor of the server-owned HistoryRouter.
|
||||
// If a regression brings IHistoryProvider back on either Galaxy driver,
|
||||
// this test fires.
|
||||
_h.RequireBoth();
|
||||
|
||||
(_h.LegacyDriver as IHistoryProvider).ShouldBeNull(
|
||||
"legacy GalaxyProxyDriver must not surface IHistoryProvider — history routes through HistoryRouter");
|
||||
(_h.MxGatewayDriver as IHistoryProvider).ShouldBeNull(
|
||||
"in-process GalaxyDriver must not surface IHistoryProvider — history routes through HistoryRouter");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user