using System; using System.IO; using System.Threading; using System.Threading.Tasks; using MessagePack; using Serilog; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Backend; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.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.Host.Tests { /// /// Validates that correctly dispatches each /// to the corresponding /// method and serializes the response into the expected response kind. Uses /// so no hardware is needed. /// [Trait("Category", "Unit")] public sealed class FwlibFrameHandlerTests { private static async Task RoundTripAsync( IFrameHandler handler, FocasMessageKind reqKind, TReq req, FocasMessageKind expectedRespKind, Action assertResponse) { using var buffer = new MemoryStream(); using var writer = new FrameWriter(buffer, leaveOpen: true); await handler.HandleAsync(reqKind, MessagePackSerializer.Serialize(req), writer, CancellationToken.None); buffer.Position = 0; using var reader = new FrameReader(buffer, leaveOpen: true); var frame = await reader.ReadFrameAsync(CancellationToken.None); frame.HasValue.ShouldBeTrue(); frame!.Value.Kind.ShouldBe(expectedRespKind); assertResponse(MessagePackSerializer.Deserialize(frame.Value.Body)); } private static FwlibFrameHandler BuildHandler() => new(new FakeFocasBackend(), new LoggerConfiguration().CreateLogger()); [Fact] public async Task OpenSession_returns_a_new_session_id() { long sessionId = 0; await RoundTripAsync( BuildHandler(), FocasMessageKind.OpenSessionRequest, new OpenSessionRequest { HostAddress = "h:8193" }, FocasMessageKind.OpenSessionResponse, resp => { resp.Success.ShouldBeTrue(); resp.SessionId.ShouldBeGreaterThan(0L); sessionId = resp.SessionId; }); sessionId.ShouldBeGreaterThan(0L); } [Fact] public async Task Read_without_open_session_returns_internal_error() { await RoundTripAsync( BuildHandler(), FocasMessageKind.ReadRequest, new ReadRequest { SessionId = 999, Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 }, DataType = FocasDataTypeCode.Int32, }, FocasMessageKind.ReadResponse, resp => { resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("session-not-open"); }); } [Fact] public async Task Full_open_write_read_round_trip_preserves_value() { var handler = BuildHandler(); // Open. using var buffer = new MemoryStream(); using var writer = new FrameWriter(buffer, leaveOpen: true); await handler.HandleAsync(FocasMessageKind.OpenSessionRequest, MessagePackSerializer.Serialize(new OpenSessionRequest { HostAddress = "h:8193" }), writer, CancellationToken.None); buffer.Position = 0; using var reader = new FrameReader(buffer, leaveOpen: true); var openFrame = await reader.ReadFrameAsync(CancellationToken.None); var openResp = MessagePackSerializer.Deserialize(openFrame!.Value.Body); var sessionId = openResp.SessionId; // Write 42 at MACRO:500 as Int32. buffer.Position = 0; buffer.SetLength(0); await handler.HandleAsync(FocasMessageKind.WriteRequest, MessagePackSerializer.Serialize(new WriteRequest { SessionId = sessionId, Address = new FocasAddressDto { Kind = 2, Number = 500 }, DataType = FocasDataTypeCode.Int32, ValueTypeCode = FocasDataTypeCode.Int32, ValueBytes = MessagePackSerializer.Serialize((int)42), }), writer, CancellationToken.None); // Read back. buffer.Position = 0; buffer.SetLength(0); await handler.HandleAsync(FocasMessageKind.ReadRequest, MessagePackSerializer.Serialize(new ReadRequest { SessionId = sessionId, Address = new FocasAddressDto { Kind = 2, Number = 500 }, DataType = FocasDataTypeCode.Int32, }), writer, CancellationToken.None); buffer.Position = 0; var readFrame = await reader.ReadFrameAsync(CancellationToken.None); readFrame.HasValue.ShouldBeTrue(); readFrame!.Value.Kind.ShouldBe(FocasMessageKind.ReadResponse); // With buffer reuse there may be multiple queued frames; we want the last one. var lastResp = MessagePackSerializer.Deserialize(readFrame.Value.Body); // If the Write frame is first, drain it. if (lastResp.ValueBytes is null) { var next = await reader.ReadFrameAsync(CancellationToken.None); lastResp = MessagePackSerializer.Deserialize(next!.Value.Body); } lastResp.Success.ShouldBeTrue(); MessagePackSerializer.Deserialize(lastResp.ValueBytes!).ShouldBe(42); } [Fact] public async Task PmcBitWrite_sets_specified_bit() { var handler = BuildHandler(); using var buffer = new MemoryStream(); using var writer = new FrameWriter(buffer, leaveOpen: true); await handler.HandleAsync(FocasMessageKind.OpenSessionRequest, MessagePackSerializer.Serialize(new OpenSessionRequest { HostAddress = "h:8193" }), writer, CancellationToken.None); buffer.Position = 0; using var reader = new FrameReader(buffer, leaveOpen: true); var openFrame = await reader.ReadFrameAsync(CancellationToken.None); var sessionId = MessagePackSerializer.Deserialize(openFrame!.Value.Body).SessionId; buffer.Position = 0; buffer.SetLength(0); await handler.HandleAsync(FocasMessageKind.PmcBitWriteRequest, MessagePackSerializer.Serialize(new PmcBitWriteRequest { SessionId = sessionId, Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 }, BitIndex = 3, Value = true, }), writer, CancellationToken.None); buffer.Position = 0; var resp = MessagePackSerializer.Deserialize( (await reader.ReadFrameAsync(CancellationToken.None))!.Value.Body); resp.Success.ShouldBeTrue(); resp.StatusCode.ShouldBe(0u); } [Fact] public async Task Probe_reports_healthy_when_session_open() { var handler = BuildHandler(); using var buffer = new MemoryStream(); using var writer = new FrameWriter(buffer, leaveOpen: true); await handler.HandleAsync(FocasMessageKind.OpenSessionRequest, MessagePackSerializer.Serialize(new OpenSessionRequest { HostAddress = "h:8193" }), writer, CancellationToken.None); buffer.Position = 0; using var reader = new FrameReader(buffer, leaveOpen: true); var sessionId = MessagePackSerializer.Deserialize( (await reader.ReadFrameAsync(CancellationToken.None))!.Value.Body).SessionId; buffer.Position = 0; buffer.SetLength(0); await handler.HandleAsync(FocasMessageKind.ProbeRequest, MessagePackSerializer.Serialize(new ProbeRequest { SessionId = sessionId }), writer, CancellationToken.None); buffer.Position = 0; var resp = MessagePackSerializer.Deserialize( (await reader.ReadFrameAsync(CancellationToken.None))!.Value.Body); resp.Healthy.ShouldBeTrue(); } [Fact] public async Task Unconfigured_backend_returns_pointed_error_message() { var handler = new FwlibFrameHandler(new UnconfiguredFocasBackend(), new LoggerConfiguration().CreateLogger()); await RoundTripAsync( handler, FocasMessageKind.OpenSessionRequest, new OpenSessionRequest { HostAddress = "h:8193" }, FocasMessageKind.OpenSessionResponse, resp => { resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("Fwlib32"); resp.ErrorCode.ShouldBe("NoFwlibBackend"); }); } } }