using System.Diagnostics; using System.Net.Sockets; using System.Reflection; using System.Security.Principal; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E; /// /// Spawns one OtOpcUa.Driver.Galaxy.Host.exe subprocess per test class and exposes /// a connected for the tests. Per Phase 2 plan §"Stream E /// Parity Validation": the Proxy owns a session against a real out-of-process Host running /// the production-shape MxAccessGalaxyBackend backed by live ZB + MXAccess COM. /// Skipped when the Host EXE isn't built, when ZB SQL is unreachable, or when the dev box /// runs as Administrator (the IPC ACL explicitly denies Administrators per decision #76). /// public sealed class ParityFixture : IAsyncLifetime { public GalaxyProxyDriver? Driver { get; private set; } public string? SkipReason { get; private set; } private Process? _host; private const string Secret = "parity-suite-secret"; public async ValueTask InitializeAsync() { if (!OperatingSystem.IsWindows()) { SkipReason = "Windows-only"; return; } if (IsAdministrator()) { SkipReason = "PipeAcl denies Administrators on dev shells"; return; } if (!await ZbReachableAsync()) { SkipReason = "Galaxy ZB SQL not reachable on localhost:1433"; return; } var hostExe = FindHostExe(); if (hostExe is null) { SkipReason = "Galaxy.Host EXE not built — run `dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host`"; return; } // Use the SQL-only DB backend by default — exercises the full IPC + dispatcher + SQL // path without requiring a healthy MXAccess connection. Tests that need MXAccess // override via env vars before InitializeAsync is called (use a separate fixture). var pipe = $"OtOpcUaGalaxyParity-{Guid.NewGuid():N}"; using var identity = WindowsIdentity.GetCurrent(); var sid = identity.User!; var psi = new ProcessStartInfo(hostExe) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, EnvironmentVariables = { ["OTOPCUA_GALAXY_PIPE"] = pipe, ["OTOPCUA_ALLOWED_SID"] = sid.Value, ["OTOPCUA_GALAXY_SECRET"] = Secret, ["OTOPCUA_GALAXY_BACKEND"] = "db", ["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;", }, }; _host = Process.Start(psi) ?? throw new InvalidOperationException("Failed to spawn Galaxy.Host EXE"); // Give the PipeServer ~2s to bind. The supervisor's HeartbeatMonitor can do this // in production with retry, but the parity tests are best served by a fixed warm-up. await Task.Delay(2_000); Driver = new GalaxyProxyDriver(new GalaxyProxyOptions { DriverInstanceId = "parity", PipeName = pipe, SharedSecret = Secret, ConnectTimeout = TimeSpan.FromSeconds(5), }); await Driver.InitializeAsync(driverConfigJson: "{}", CancellationToken.None); } public async ValueTask DisposeAsync() { if (Driver is not null) { try { await Driver.ShutdownAsync(CancellationToken.None); } catch { /* shutdown */ } Driver.Dispose(); } if (_host is not null && !_host.HasExited) { try { _host.Kill(entireProcessTree: true); } catch { /* ignore */ } try { _host.WaitForExit(5_000); } catch { /* ignore */ } } _host?.Dispose(); } /// Skip the test if the fixture couldn't initialize. xUnit Skip.If pattern. public void SkipIfUnavailable() { if (SkipReason is not null) Assert.Skip(SkipReason); } private static bool IsAdministrator() { if (!OperatingSystem.IsWindows()) return false; using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } private static async Task ZbReachableAsync() { try { using var client = new TcpClient(); var task = client.ConnectAsync("localhost", 1433); return await Task.WhenAny(task, Task.Delay(1_500)) == task && client.Connected; } catch { return false; } } private static string? FindHostExe() { var asmDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var solutionRoot = asmDir; for (var i = 0; i < 8 && solutionRoot is not null; i++) { if (File.Exists(Path.Combine(solutionRoot, "ZB.MOM.WW.OtOpcUa.slnx"))) break; solutionRoot = Path.GetDirectoryName(solutionRoot); } if (solutionRoot is null) return null; var path = Path.Combine(solutionRoot, "src", "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host", "bin", "Debug", "net48", "OtOpcUa.Driver.Galaxy.Host.exe"); return File.Exists(path) ? path : null; } } [CollectionDefinition(nameof(ParityCollection))] public sealed class ParityCollection : ICollectionFixture { }