192 lines
7.1 KiB
C#
192 lines
7.1 KiB
C#
using System.Security.Principal;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Ipc;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests;
|
|
|
|
/// <summary>
|
|
/// Drives every <see cref="MessageKind"/> through the full IPC stack — Host
|
|
/// <see cref="GalaxyFrameHandler"/> backed by <see cref="StubGalaxyBackend"/> on one end,
|
|
/// <see cref="GalaxyProxyDriver"/> on the other — to prove the wire protocol, dispatcher,
|
|
/// and capability forwarding agree end-to-end. The "stub backend" replies with success for
|
|
/// lifecycle/subscribe/recycle and a recognizable "not-implemented" error for the data-plane
|
|
/// calls that need the deferred MXAccess lift; the test asserts both shapes.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public sealed class EndToEndIpcTests
|
|
{
|
|
private static bool IsAdministrator()
|
|
{
|
|
if (!OperatingSystem.IsWindows()) return false;
|
|
using var identity = WindowsIdentity.GetCurrent();
|
|
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
|
|
}
|
|
|
|
private static (string Pipe, string Secret, SecurityIdentifier Sid) MakeIpcParams() =>
|
|
($"OtOpcUaGalaxyE2E-{Guid.NewGuid():N}",
|
|
"e2e-secret",
|
|
WindowsIdentity.GetCurrent().User!);
|
|
|
|
private static async Task<(GalaxyProxyDriver Driver, CancellationTokenSource Cts, Task ServerTask, PipeServer Server)>
|
|
StartStackAsync()
|
|
{
|
|
var (pipe, secret, sid) = MakeIpcParams();
|
|
Logger log = new LoggerConfiguration().CreateLogger();
|
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
|
|
|
var server = new PipeServer(pipe, sid, secret, log);
|
|
var backend = new StubGalaxyBackend();
|
|
var handler = new GalaxyFrameHandler(backend, log);
|
|
var serverTask = Task.Run(() => server.RunAsync(handler, cts.Token));
|
|
|
|
var driver = new GalaxyProxyDriver(new GalaxyProxyOptions
|
|
{
|
|
DriverInstanceId = "gal-e2e",
|
|
PipeName = pipe,
|
|
SharedSecret = secret,
|
|
ConnectTimeout = TimeSpan.FromSeconds(5),
|
|
});
|
|
|
|
await driver.InitializeAsync(driverConfigJson: "{}", cts.Token);
|
|
return (driver, cts, serverTask, server);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Initialize_succeeds_via_OpenSession_and_health_goes_Healthy()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
|
|
|
|
var (driver, cts, serverTask, server) = await StartStackAsync();
|
|
try
|
|
{
|
|
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
finally
|
|
{
|
|
await driver.ShutdownAsync(CancellationToken.None);
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { /* shutdown */ }
|
|
server.Dispose();
|
|
driver.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_returns_Bad_status_for_each_requested_reference_until_backend_lifted()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
|
|
|
|
var (driver, cts, serverTask, server) = await StartStackAsync();
|
|
try
|
|
{
|
|
// Stub backend currently fails the whole batch with a "not-implemented" error;
|
|
// the driver surfaces this as InvalidOperationException with the error text.
|
|
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
|
|
driver.ReadAsync(["TagA", "TagB"], cts.Token));
|
|
ex.Message.ShouldContain("MXAccess code lift pending");
|
|
}
|
|
finally
|
|
{
|
|
await driver.ShutdownAsync(CancellationToken.None);
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
server.Dispose();
|
|
driver.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_returns_per_tag_BadInternalError_status_until_backend_lifted()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
|
|
|
|
var (driver, cts, serverTask, server) = await StartStackAsync();
|
|
try
|
|
{
|
|
// Stub backend's WriteValuesAsync returns a per-tag bad status — the proxy
|
|
// surfaces those without throwing.
|
|
var results = await driver.WriteAsync([new WriteRequest("TagA", 42)], cts.Token);
|
|
results.Count.ShouldBe(1);
|
|
results[0].StatusCode.ShouldBe(0x80020000u); // Bad_InternalError
|
|
}
|
|
finally
|
|
{
|
|
await driver.ShutdownAsync(CancellationToken.None);
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
server.Dispose();
|
|
driver.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Subscribe_returns_handle_then_Unsubscribe_closes_cleanly()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
|
|
|
|
var (driver, cts, serverTask, server) = await StartStackAsync();
|
|
try
|
|
{
|
|
var handle = await driver.SubscribeAsync(
|
|
["TagA"], TimeSpan.FromMilliseconds(500), cts.Token);
|
|
handle.DiagnosticId.ShouldStartWith("galaxy-sub-");
|
|
|
|
await driver.UnsubscribeAsync(handle, cts.Token); // one-way; just verify no throw
|
|
}
|
|
finally
|
|
{
|
|
await driver.ShutdownAsync(CancellationToken.None);
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
server.Dispose();
|
|
driver.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubscribeAlarms_and_Acknowledge_round_trip_without_errors()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
|
|
|
|
var (driver, cts, serverTask, server) = await StartStackAsync();
|
|
try
|
|
{
|
|
var handle = await driver.SubscribeAlarmsAsync(["Eq001"], cts.Token);
|
|
handle.DiagnosticId.ShouldNotBeNullOrEmpty();
|
|
|
|
await driver.AcknowledgeAsync(
|
|
[new AlarmAcknowledgeRequest("Eq001", "evt-1", "test ack")],
|
|
cts.Token);
|
|
}
|
|
finally
|
|
{
|
|
await driver.ShutdownAsync(CancellationToken.None);
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
server.Dispose();
|
|
driver.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadProcessed_throws_NotSupported_immediately_without_round_trip()
|
|
{
|
|
// No IPC needed — the proxy short-circuits to NotSupportedException per the v2 design
|
|
// (Galaxy Historian only supports raw reads; processed reads are an OPC UA aggregate
|
|
// computed by the OPC UA stack, not the driver).
|
|
var driver = new GalaxyProxyDriver(new GalaxyProxyOptions
|
|
{
|
|
DriverInstanceId = "gal-stub", PipeName = "x", SharedSecret = "x",
|
|
});
|
|
await Should.ThrowAsync<NotSupportedException>(() =>
|
|
driver.ReadProcessedAsync("TagA", DateTime.UtcNow.AddHours(-1), DateTime.UtcNow,
|
|
TimeSpan.FromMinutes(1), HistoryAggregateType.Average, CancellationToken.None));
|
|
}
|
|
}
|