131 lines
5.3 KiB
C#
131 lines
5.3 KiB
C#
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Security.Principal;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Ipc;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests;
|
|
|
|
/// <summary>
|
|
/// The honest cross-FX parity test — spawns the actual <c>OtOpcUa.Driver.Galaxy.Host.exe</c>
|
|
/// subprocess (net48 x86), the Proxy connects via real named pipe, exercises Discover
|
|
/// against the live Galaxy ZB DB, and asserts gobjects come back. This is the production
|
|
/// deployment shape (Tier C: separate process, IPC over named pipe, Proxy in the .NET 10
|
|
/// server process). Skipped when the Host EXE isn't built or Galaxy is unreachable.
|
|
/// </summary>
|
|
[Trait("Category", "ProcessSpawnParity")]
|
|
public sealed class HostSubprocessParityTests : IDisposable
|
|
{
|
|
private Process? _hostProcess;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_hostProcess is not null && !_hostProcess.HasExited)
|
|
{
|
|
try { _hostProcess.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
|
try { _hostProcess.WaitForExit(5_000); } catch { /* ignore */ }
|
|
}
|
|
_hostProcess?.Dispose();
|
|
}
|
|
|
|
private static string? FindHostExe()
|
|
{
|
|
// The test assembly lives at tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/bin/Debug/net10.0/.
|
|
// The Host EXE lives at src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/.
|
|
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 candidate = Path.Combine(solutionRoot,
|
|
"src", "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host", "bin", "Debug", "net48",
|
|
"OtOpcUa.Driver.Galaxy.Host.exe");
|
|
return File.Exists(candidate) ? candidate : null;
|
|
}
|
|
|
|
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<bool> ZbReachableAsync()
|
|
{
|
|
try
|
|
{
|
|
using var client = new System.Net.Sockets.TcpClient();
|
|
var task = client.ConnectAsync("localhost", 1433);
|
|
return await Task.WhenAny(task, Task.Delay(1_500)) == task && client.Connected;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Spawned_Host_in_db_mode_lets_Proxy_Discover_real_Galaxy_gobjects()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
|
|
if (!await ZbReachableAsync()) return;
|
|
|
|
var hostExe = FindHostExe();
|
|
if (hostExe is null) return; // skip when the Host hasn't been built
|
|
|
|
using var identity = WindowsIdentity.GetCurrent();
|
|
var sid = identity.User!;
|
|
var pipeName = $"OtOpcUaGalaxyParity-{Guid.NewGuid():N}";
|
|
const string secret = "parity-secret";
|
|
|
|
var psi = new ProcessStartInfo(hostExe)
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
EnvironmentVariables =
|
|
{
|
|
["OTOPCUA_GALAXY_PIPE"] = pipeName,
|
|
["OTOPCUA_ALLOWED_SID"] = sid.Value,
|
|
["OTOPCUA_GALAXY_SECRET"] = secret,
|
|
["OTOPCUA_GALAXY_BACKEND"] = "db", // SQL-only — doesn't need MXAccess
|
|
["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
|
},
|
|
};
|
|
|
|
_hostProcess = Process.Start(psi)
|
|
?? throw new InvalidOperationException("Failed to spawn Galaxy.Host");
|
|
|
|
// Wait for the pipe to come up — the Host's PipeServer takes ~100ms to bind.
|
|
await Task.Delay(2_000);
|
|
|
|
await using var client = await GalaxyIpcClient.ConnectAsync(
|
|
pipeName, secret, TimeSpan.FromSeconds(5), CancellationToken.None);
|
|
|
|
var sessionResp = await client.CallAsync<OpenSessionRequest, OpenSessionResponse>(
|
|
MessageKind.OpenSessionRequest,
|
|
new OpenSessionRequest { DriverInstanceId = "parity", DriverConfigJson = "{}" },
|
|
MessageKind.OpenSessionResponse,
|
|
CancellationToken.None);
|
|
sessionResp.Success.ShouldBeTrue(sessionResp.Error);
|
|
|
|
var discoverResp = await client.CallAsync<DiscoverHierarchyRequest, DiscoverHierarchyResponse>(
|
|
MessageKind.DiscoverHierarchyRequest,
|
|
new DiscoverHierarchyRequest { SessionId = sessionResp.SessionId },
|
|
MessageKind.DiscoverHierarchyResponse,
|
|
CancellationToken.None);
|
|
|
|
discoverResp.Success.ShouldBeTrue(discoverResp.Error);
|
|
discoverResp.Objects.Length.ShouldBeGreaterThan(0,
|
|
"live Galaxy ZB has at least one deployed gobject");
|
|
|
|
await client.SendOneWayAsync(MessageKind.CloseSessionRequest,
|
|
new CloseSessionRequest { SessionId = sessionResp.SessionId }, CancellationToken.None);
|
|
}
|
|
}
|