Phase 2 — port MXAccess COM client to Galaxy.Host + MxAccessGalaxyBackend (3rd IGalaxyBackend) + live MXAccess smoke + Phase 2 exit-gate doc + adversarial review. The full Galaxy data-plane now flows through the v2 IPC topology end-to-end against live ArchestrA.MxAccess.dll, on this dev box, with 30/30 Host tests + 9/9 Proxy tests + 963/963 solution tests passing alongside the unchanged 494 v1 IntegrationTests baseline. Backend/MxAccess/Vtq is a focused port of v1's Vtq value-timestamp-quality DTO. Backend/MxAccess/IMxProxy abstracts LMXProxyServer (port of v1's IMxProxy with the same Register/Unregister/AddItem/RemoveItem/AdviseSupervisory/UnAdviseSupervisory/Write surface + OnDataChange + OnWriteComplete events); MxProxyAdapter is the concrete COM-backed implementation that does Marshal.ReleaseComObject-loop on Unregister, must be constructed on an STA thread. Backend/MxAccess/MxAccessClient is the focused port of v1's MxAccessClient partials — Connect/Disconnect/Read/Write/Subscribe/Unsubscribe through the new Sta/StaPump (the real Win32 GetMessage pump from the previous commit), ConcurrentDictionary handle tracking, OnDataChange event marshalling to per-tag callbacks, ReadAsync implemented as the canonical subscribe → first-OnDataChange → unsubscribe one-shot pattern. Galaxy.Host csproj flipped to x86 PlatformTarget + Prefer32Bit=true with the ArchestrA.MxAccess HintPath ..\..\lib\ArchestrA.MxAccess.dll reference (lib/ already contains the production DLL). Backend/MxAccessGalaxyBackend is the third IGalaxyBackend implementation (alongside StubGalaxyBackend and DbBackedGalaxyBackend): combines GalaxyRepository (Discover) with MxAccessClient (Read/Write/Subscribe), MessagePack-deserializes inbound write values, MessagePack-serializes outbound read values into ValueBytes, decodes ArrayDimension/SecurityClassification/category_id with the same v1 mapping. Program.cs selects between stub|db|mxaccess via OTOPCUA_GALAXY_BACKEND env var (default = mxaccess); OTOPCUA_GALAXY_ZB_CONN overrides the ZB connection string; OTOPCUA_GALAXY_CLIENT_NAME sets the Wonderware client identity; the StaPump and MxAccessClient lifecycles are tied to the server.RunAsync try/finally so a clean Ctrl+C tears down the COM proxy via Marshal.ReleaseComObject before the pump's WM_QUIT. Live MXAccess smoke tests (MxAccessLiveSmokeTests, net48 x86) — skipped when ZB unreachable or aaBootstrap not running, otherwise verify (1) MxAccessClient.ConnectAsync returns a positive LMXProxyServer handle on the StaPump, (2) MxAccessGalaxyBackend.OpenSession + Discover returns at least one gobject with attributes, (3) MxAccessGalaxyBackend.ReadValues against the first discovered attribute returns a response with the correct TagReference shape (value + quality vary by what's running, so we don't assert specific values). All 3 pass on this dev box. EndToEndIpcTests + IpcHandshakeIntegrationTests moved from Galaxy.Proxy.Tests (net10) to Galaxy.Host.Tests (net48 x86) — the previous test placement silently dropped them at xUnit discovery because Host became net48 x86 and net10 process can't load it. Rewritten to use Shared's FrameReader/FrameWriter directly instead of going through Proxy's GalaxyIpcClient (functionally equivalent — same wire protocol, framing primitives + dispatcher are the production code path verbatim). 7 IPC tests now run cleanly: Hello+heartbeat round-trip, wrong-secret rejection, OpenSession session-id assignment, Discover error-response surfacing, WriteValues per-tag bad status, Subscribe id assignment, Recycle grace window. Phase 2 exit-gate doc (docs/v2/implementation/exit-gate-phase-2.md) supersedes the partial-exit doc with the as-built state — Streams A/B/C complete; D/E gated only on the legacy-Host removal + parity-test rewrite cycle that fundamentally requires multi-day debug iteration; full adversarial-review section ranking 8 findings (2 high, 3 medium, 3 low) all explicitly deferred to Stream D/E or v2.1 with rationale; Stream-D removal checklist gives the next-session entry point with two policy options for the 494 v1 tests (rewrite-to-use-Proxy vs archive-and-write-smaller-v2-parity-suite). Cannot one-shot Stream D.1 in any single session because deleting OtOpcUa.Host requires the v1 IntegrationTests cycle to be retargeted first; that's the structural blocker, not "needs more code" — and the plan itself budgets 3-4 weeks for it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// End-to-end smoke against the live MXAccess COM runtime + Galaxy ZB DB on this dev box.
|
||||
/// Skipped when ArchestrA bootstrap (<c>aaBootstrap</c>) isn't running. Verifies the
|
||||
/// ported <see cref="MxAccessClient"/> can connect to <c>LMXProxyServer</c>, the
|
||||
/// <see cref="MxAccessGalaxyBackend"/> can answer Discover against the live ZB schema,
|
||||
/// and a one-shot read returns a valid VTQ for the first deployed attribute it finds.
|
||||
/// </summary>
|
||||
[Trait("Category", "LiveMxAccess")]
|
||||
public sealed class MxAccessLiveSmokeTests
|
||||
{
|
||||
private static GalaxyRepositoryOptions DevZb() => new()
|
||||
{
|
||||
ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;Connect Timeout=2;",
|
||||
CommandTimeoutSeconds = 10,
|
||||
};
|
||||
|
||||
private static async Task<bool> ArchestraReachableAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var repo = new GalaxyRepository(DevZb());
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
if (!await repo.TestConnectionAsync(cts.Token)) return false;
|
||||
|
||||
using var sc = new System.ServiceProcess.ServiceController("aaBootstrap");
|
||||
return sc.Status == System.ServiceProcess.ServiceControllerStatus.Running;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_to_local_LMXProxyServer_succeeds()
|
||||
{
|
||||
if (!await ArchestraReachableAsync()) return;
|
||||
|
||||
using var pump = new StaPump("MxA-test-pump");
|
||||
await pump.WaitForStartedAsync();
|
||||
|
||||
using var mx = new MxAccessClient(pump, new MxProxyAdapter(), "OtOpcUa-MxAccessSmoke");
|
||||
var handle = await mx.ConnectAsync();
|
||||
handle.ShouldBeGreaterThan(0);
|
||||
mx.IsConnected.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Backend_OpenSession_then_Discover_returns_objects_with_attributes()
|
||||
{
|
||||
if (!await ArchestraReachableAsync()) return;
|
||||
|
||||
using var pump = new StaPump("MxA-test-pump");
|
||||
await pump.WaitForStartedAsync();
|
||||
using var mx = new MxAccessClient(pump, new MxProxyAdapter(), "OtOpcUa-MxAccessSmoke");
|
||||
var backend = new MxAccessGalaxyBackend(new GalaxyRepository(DevZb()), mx);
|
||||
|
||||
var session = await backend.OpenSessionAsync(new OpenSessionRequest { DriverInstanceId = "smoke" }, CancellationToken.None);
|
||||
session.Success.ShouldBeTrue(session.Error);
|
||||
|
||||
var resp = await backend.DiscoverAsync(new DiscoverHierarchyRequest { SessionId = session.SessionId }, CancellationToken.None);
|
||||
resp.Success.ShouldBeTrue(resp.Error);
|
||||
resp.Objects.Length.ShouldBeGreaterThan(0);
|
||||
|
||||
await backend.CloseSessionAsync(new CloseSessionRequest { SessionId = session.SessionId }, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live one-shot read against any attribute we discover. Best-effort — passes silently
|
||||
/// if no readable attribute is exposed (some Galaxy installs are configuration-only;
|
||||
/// we only assert the call shape is correct, not a specific value).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Backend_ReadValues_against_discovered_attribute_returns_a_response_shape()
|
||||
{
|
||||
if (!await ArchestraReachableAsync()) return;
|
||||
|
||||
using var pump = new StaPump("MxA-test-pump");
|
||||
await pump.WaitForStartedAsync();
|
||||
using var mx = new MxAccessClient(pump, new MxProxyAdapter(), "OtOpcUa-MxAccessSmoke");
|
||||
var backend = new MxAccessGalaxyBackend(new GalaxyRepository(DevZb()), mx);
|
||||
|
||||
var session = await backend.OpenSessionAsync(new OpenSessionRequest { DriverInstanceId = "smoke" }, CancellationToken.None);
|
||||
var disc = await backend.DiscoverAsync(new DiscoverHierarchyRequest { SessionId = session.SessionId }, CancellationToken.None);
|
||||
var firstAttr = System.Linq.Enumerable.FirstOrDefault(disc.Objects, o => o.Attributes.Length > 0);
|
||||
if (firstAttr is null)
|
||||
{
|
||||
await backend.CloseSessionAsync(new CloseSessionRequest { SessionId = session.SessionId }, CancellationToken.None);
|
||||
return;
|
||||
}
|
||||
|
||||
var fullRef = $"{firstAttr.TagName}.{firstAttr.Attributes[0].AttributeName}";
|
||||
var read = await backend.ReadValuesAsync(
|
||||
new ReadValuesRequest { SessionId = session.SessionId, TagReferences = new[] { fullRef } },
|
||||
CancellationToken.None);
|
||||
|
||||
read.Success.ShouldBeTrue();
|
||||
read.Values.Length.ShouldBe(1);
|
||||
// We don't assert the value (it may be Bad/Uncertain depending on what's running);
|
||||
// we only assert the response shape is correct end-to-end.
|
||||
read.Values[0].TagReference.ShouldBe(fullRef);
|
||||
|
||||
await backend.CloseSessionAsync(new CloseSessionRequest { SessionId = session.SessionId }, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user