After running the matrix end-to-end against the live rig for the first time, three of the nine failures were false positives — bugs in the harness and test invariants, not real backend deltas: 1. ParityHarness configured the legacy backend with OTOPCUA_GALAXY_BACKEND=db, which is Discover-only. Reads, writes, and reinits all returned "MXAccess code lift pending — DB-backed backend covers Discover only". Switched to mxaccess backend; the ZB connection string still drives the discovery path. 2. HistoryReadParityTests asserted "neither backend implements IHistoryProvider" — but the legacy GalaxyProxyDriver still does (it's an accepted back-compat delta retired in PR 7.2). The architectural pin we *want* is "the new path doesn't regress to per-driver history", so the test now asserts only the mxgw side. 3. AlarmTransitionParityTests strict-pinned the five sub-attribute refs (InAlarmRef, etc.) on the legacy condition. PR 2.1 added those refs specifically so the new mxgw driver could populate them via AlarmRefBuilder; legacy pre-dates PR 2.1 and leaves them null — that's correct, not a regression. Test now asserts a one-way invariant: when legacy populated a ref, mxgw must match. When legacy is null, mxgw is free to populate (the mxgw → server-side AlarmConditionService direction). The six remaining failures are real: - 2 from the gw-side `[]` array suffix (filed in mxaccessgw/requirements-array-suffix-fix.md) - 2 write-StatusCode mapping deltas (0x80050000 vs 0x80020000) — Bad-status both ways but mapped to different OPC UA codes - 1 event-rate ratio of 5x (mxgw dispatches 5x legacy in the same 3s window) - (Plus the 2 ScanState scenarios that skip cleanly — single-platform rig as documented) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
3.0 KiB
C#
70 lines
3.0 KiB
C#
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 The_new_Galaxy_backend_does_not_implement_IHistoryProvider_directly()
|
|
{
|
|
// Pinning the architectural decision from Phase 1 (PR 1.3): per-driver
|
|
// IHistoryProvider was retired in favor of the server-owned HistoryRouter
|
|
// for the *new* in-process GalaxyDriver. The legacy GalaxyProxyDriver
|
|
// still surfaces IHistoryProvider for back-compat with the legacy server
|
|
// bootstrap path (it's an accepted delta — the legacy driver retires in
|
|
// PR 7.2 alongside the rest of the legacy projects). The architectural
|
|
// pin we want to enforce is "the *new* path doesn't regress to per-driver
|
|
// history".
|
|
_h.RequireBoth();
|
|
|
|
(_h.MxGatewayDriver as IHistoryProvider).ShouldBeNull(
|
|
"in-process GalaxyDriver must not surface IHistoryProvider — history routes through HistoryRouter");
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|