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 { }