266 lines
11 KiB
C#
266 lines
11 KiB
C#
using MessagePack;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Ipc;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
/// <summary>
|
|
/// End-to-end IPC round-trips over an in-memory loopback: <c>IpcFocasClient</c> talks
|
|
/// to a test fake that plays the Host's role by reading frames, dispatching on kind,
|
|
/// and responding with canned DTOs. Validates that every <see cref="IFocasClient"/>
|
|
/// method translates to the right wire frame + decodes the response correctly.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class IpcFocasClientTests
|
|
{
|
|
private const string Secret = "test-secret";
|
|
|
|
private static async Task ServerLoopAsync(Stream serverSide, Func<FocasMessageKind, byte[], FrameWriter, Task> dispatch, CancellationToken ct)
|
|
{
|
|
using var reader = new FrameReader(serverSide, leaveOpen: true);
|
|
using var writer = new FrameWriter(serverSide, leaveOpen: true);
|
|
|
|
// Hello handshake.
|
|
var first = await reader.ReadFrameAsync(ct);
|
|
if (first is null) return;
|
|
var hello = MessagePackSerializer.Deserialize<Hello>(first.Value.Body);
|
|
var accepted = hello.SharedSecret == Secret;
|
|
await writer.WriteAsync(FocasMessageKind.HelloAck,
|
|
new HelloAck { Accepted = accepted, RejectReason = accepted ? null : "wrong-secret" }, ct);
|
|
if (!accepted) return;
|
|
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
var frame = await reader.ReadFrameAsync(ct);
|
|
if (frame is null) return;
|
|
await dispatch(frame.Value.Kind, frame.Value.Body, writer);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Connect_sends_OpenSessionRequest_and_caches_session_id()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
OpenSessionRequest? received = null;
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
if (kind == FocasMessageKind.OpenSessionRequest)
|
|
{
|
|
received = MessagePackSerializer.Deserialize<OpenSessionRequest>(body);
|
|
await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
|
|
new OpenSessionResponse { Success = true, SessionId = 42 }, cts.Token);
|
|
}
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc, FocasCncSeries.Thirty_i);
|
|
await client.ConnectAsync(new FocasHostAddress("192.168.1.50", 8193), TimeSpan.FromSeconds(2), cts.Token);
|
|
|
|
client.IsConnected.ShouldBeTrue();
|
|
received.ShouldNotBeNull();
|
|
received!.HostAddress.ShouldBe("192.168.1.50:8193");
|
|
received.CncSeries.ShouldBe((int)FocasCncSeries.Thirty_i);
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Connect_throws_when_host_rejects()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
if (kind == FocasMessageKind.OpenSessionRequest)
|
|
{
|
|
await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
|
|
new OpenSessionResponse { Success = false, Error = "unreachable", ErrorCode = "EW_SOCKET" }, cts.Token);
|
|
}
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc);
|
|
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
|
await client.ConnectAsync(new FocasHostAddress("10.0.0.1", 8193), TimeSpan.FromSeconds(1), cts.Token));
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_sends_ReadRequest_and_decodes_response()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
ReadRequest? received = null;
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
switch (kind)
|
|
{
|
|
case FocasMessageKind.OpenSessionRequest:
|
|
await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
|
|
new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
|
|
break;
|
|
case FocasMessageKind.ReadRequest:
|
|
received = MessagePackSerializer.Deserialize<ReadRequest>(body);
|
|
await writer.WriteAsync(FocasMessageKind.ReadResponse,
|
|
new ReadResponse
|
|
{
|
|
Success = true,
|
|
StatusCode = 0,
|
|
ValueBytes = MessagePackSerializer.Serialize((int)12345),
|
|
ValueTypeCode = FocasDataTypeCode.Int32,
|
|
}, cts.Token);
|
|
break;
|
|
}
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc);
|
|
await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);
|
|
|
|
var addr = new FocasAddress(FocasAreaKind.Parameter, null, 1815, null);
|
|
var (value, status) = await client.ReadAsync(addr, FocasDataType.Int32, cts.Token);
|
|
status.ShouldBe(0u);
|
|
value.ShouldBe(12345);
|
|
received!.Address.Number.ShouldBe(1815);
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_sends_WriteRequest_and_returns_status()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
switch (kind)
|
|
{
|
|
case FocasMessageKind.OpenSessionRequest:
|
|
await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
|
|
new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
|
|
break;
|
|
case FocasMessageKind.WriteRequest:
|
|
var req = MessagePackSerializer.Deserialize<WriteRequest>(body);
|
|
MessagePackSerializer.Deserialize<double>(req.ValueBytes!).ShouldBe(3.14);
|
|
await writer.WriteAsync(FocasMessageKind.WriteResponse,
|
|
new WriteResponse { Success = true, StatusCode = 0 }, cts.Token);
|
|
break;
|
|
}
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc);
|
|
await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);
|
|
|
|
var status = await client.WriteAsync(new FocasAddress(FocasAreaKind.Macro, null, 500, null),
|
|
FocasDataType.Float64, 3.14, cts.Token);
|
|
status.ShouldBe(0u);
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_pmc_bit_sends_first_class_RMW_frame()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
PmcBitWriteRequest? received = null;
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
switch (kind)
|
|
{
|
|
case FocasMessageKind.OpenSessionRequest:
|
|
await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
|
|
new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
|
|
break;
|
|
case FocasMessageKind.PmcBitWriteRequest:
|
|
received = MessagePackSerializer.Deserialize<PmcBitWriteRequest>(body);
|
|
await writer.WriteAsync(FocasMessageKind.PmcBitWriteResponse,
|
|
new PmcBitWriteResponse { Success = true, StatusCode = 0 }, cts.Token);
|
|
break;
|
|
}
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc);
|
|
await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);
|
|
|
|
var addr = new FocasAddress(FocasAreaKind.Pmc, "R", 100, BitIndex: 5);
|
|
var status = await client.WriteAsync(addr, FocasDataType.Bit, true, cts.Token);
|
|
status.ShouldBe(0u);
|
|
received.ShouldNotBeNull();
|
|
received!.BitIndex.ShouldBe(5);
|
|
received.Value.ShouldBeTrue();
|
|
received.Address.PmcLetter.ShouldBe("R");
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Probe_round_trips_health_from_host()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
switch (kind)
|
|
{
|
|
case FocasMessageKind.OpenSessionRequest:
|
|
await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
|
|
new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
|
|
break;
|
|
case FocasMessageKind.ProbeRequest:
|
|
await writer.WriteAsync(FocasMessageKind.ProbeResponse,
|
|
new ProbeResponse { Healthy = true, ObservedAtUtcUnixMs = 1_700_000_000_000 }, cts.Token);
|
|
break;
|
|
}
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc);
|
|
await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);
|
|
(await client.ProbeAsync(cts.Token)).ShouldBeTrue();
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Error_response_from_host_surfaces_as_FocasIpcException()
|
|
{
|
|
await using var loop = new IpcLoopback();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
|
|
{
|
|
await writer.WriteAsync(FocasMessageKind.ErrorResponse,
|
|
new ErrorResponse { Code = "backend-exception", Message = "simulated" }, cts.Token);
|
|
}, cts.Token));
|
|
|
|
var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
|
|
var client = new IpcFocasClient(ipc);
|
|
var ex = await Should.ThrowAsync<FocasIpcException>(async () =>
|
|
await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token));
|
|
ex.Code.ShouldBe("backend-exception");
|
|
|
|
cts.Cancel();
|
|
try { await server; } catch { }
|
|
}
|
|
}
|