281 lines
9.0 KiB
C#
281 lines
9.0 KiB
C#
using MessagePack;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Tests;
|
|
|
|
/// <summary>
|
|
/// MessagePack round-trip coverage for every FOCAS IPC contract. Ensures
|
|
/// <c>[Key]</c>-tagged fields survive serialize -> deserialize without loss so the
|
|
/// wire format stays stable across Proxy (.NET 10) and Host (.NET 4.8) processes.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ContractRoundTripTests
|
|
{
|
|
private static T RoundTrip<T>(T value)
|
|
{
|
|
var bytes = MessagePackSerializer.Serialize(value);
|
|
return MessagePackSerializer.Deserialize<T>(bytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Hello_round_trips()
|
|
{
|
|
var original = new Hello
|
|
{
|
|
ProtocolMajor = 1,
|
|
ProtocolMinor = 2,
|
|
PeerName = "OtOpcUa.Server",
|
|
SharedSecret = "abc-123",
|
|
Features = ["bulk-read", "pmc-rmw"],
|
|
};
|
|
var decoded = RoundTrip(original);
|
|
decoded.ProtocolMajor.ShouldBe(1);
|
|
decoded.ProtocolMinor.ShouldBe(2);
|
|
decoded.PeerName.ShouldBe("OtOpcUa.Server");
|
|
decoded.SharedSecret.ShouldBe("abc-123");
|
|
decoded.Features.ShouldBe(["bulk-read", "pmc-rmw"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void HelloAck_rejected_carries_reason()
|
|
{
|
|
var original = new HelloAck { Accepted = false, RejectReason = "bad secret" };
|
|
var decoded = RoundTrip(original);
|
|
decoded.Accepted.ShouldBeFalse();
|
|
decoded.RejectReason.ShouldBe("bad secret");
|
|
}
|
|
|
|
[Fact]
|
|
public void Heartbeat_and_ack_preserve_ticks()
|
|
{
|
|
var hb = RoundTrip(new Heartbeat { MonotonicTicks = 987654321 });
|
|
hb.MonotonicTicks.ShouldBe(987654321);
|
|
|
|
var ack = RoundTrip(new HeartbeatAck { MonotonicTicks = 987654321, HostUtcUnixMs = 1_700_000_000_000 });
|
|
ack.MonotonicTicks.ShouldBe(987654321);
|
|
ack.HostUtcUnixMs.ShouldBe(1_700_000_000_000);
|
|
}
|
|
|
|
[Fact]
|
|
public void ErrorResponse_preserves_code_and_message()
|
|
{
|
|
var decoded = RoundTrip(new ErrorResponse { Code = "Fwlib32Crashed", Message = "EW_UNEXPECTED" });
|
|
decoded.Code.ShouldBe("Fwlib32Crashed");
|
|
decoded.Message.ShouldBe("EW_UNEXPECTED");
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenSessionRequest_preserves_series_and_timeout()
|
|
{
|
|
var decoded = RoundTrip(new OpenSessionRequest
|
|
{
|
|
HostAddress = "192.168.1.50:8193",
|
|
TimeoutMs = 3500,
|
|
CncSeries = 5,
|
|
});
|
|
decoded.HostAddress.ShouldBe("192.168.1.50:8193");
|
|
decoded.TimeoutMs.ShouldBe(3500);
|
|
decoded.CncSeries.ShouldBe(5);
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenSessionResponse_failure_carries_error_code()
|
|
{
|
|
var decoded = RoundTrip(new OpenSessionResponse
|
|
{
|
|
Success = false,
|
|
SessionId = 0,
|
|
Error = "unreachable",
|
|
ErrorCode = "EW_SOCKET",
|
|
});
|
|
decoded.Success.ShouldBeFalse();
|
|
decoded.Error.ShouldBe("unreachable");
|
|
decoded.ErrorCode.ShouldBe("EW_SOCKET");
|
|
}
|
|
|
|
[Fact]
|
|
public void FocasAddressDto_carries_pmc_with_bit_index()
|
|
{
|
|
var decoded = RoundTrip(new FocasAddressDto
|
|
{
|
|
Kind = 0,
|
|
PmcLetter = "R",
|
|
Number = 100,
|
|
BitIndex = 3,
|
|
});
|
|
decoded.Kind.ShouldBe(0);
|
|
decoded.PmcLetter.ShouldBe("R");
|
|
decoded.Number.ShouldBe(100);
|
|
decoded.BitIndex.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void FocasAddressDto_macro_omits_letter_and_bit()
|
|
{
|
|
var decoded = RoundTrip(new FocasAddressDto { Kind = 2, Number = 500 });
|
|
decoded.Kind.ShouldBe(2);
|
|
decoded.PmcLetter.ShouldBeNull();
|
|
decoded.Number.ShouldBe(500);
|
|
decoded.BitIndex.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadRequest_and_response_round_trip()
|
|
{
|
|
var req = RoundTrip(new ReadRequest
|
|
{
|
|
SessionId = 42,
|
|
Address = new FocasAddressDto { Kind = 1, Number = 1815 },
|
|
DataType = FocasDataTypeCode.Int32,
|
|
TimeoutMs = 1500,
|
|
});
|
|
req.SessionId.ShouldBe(42);
|
|
req.Address.Number.ShouldBe(1815);
|
|
req.DataType.ShouldBe(FocasDataTypeCode.Int32);
|
|
|
|
var resp = RoundTrip(new ReadResponse
|
|
{
|
|
Success = true,
|
|
StatusCode = 0,
|
|
ValueBytes = MessagePackSerializer.Serialize((int)12345),
|
|
ValueTypeCode = FocasDataTypeCode.Int32,
|
|
SourceTimestampUtcUnixMs = 1_700_000_000_000,
|
|
});
|
|
resp.Success.ShouldBeTrue();
|
|
resp.StatusCode.ShouldBe(0u);
|
|
MessagePackSerializer.Deserialize<int>(resp.ValueBytes!).ShouldBe(12345);
|
|
resp.ValueTypeCode.ShouldBe(FocasDataTypeCode.Int32);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteRequest_and_response_round_trip()
|
|
{
|
|
var req = RoundTrip(new WriteRequest
|
|
{
|
|
SessionId = 1,
|
|
Address = new FocasAddressDto { Kind = 2, Number = 500 },
|
|
DataType = FocasDataTypeCode.Float64,
|
|
ValueBytes = MessagePackSerializer.Serialize(3.14159),
|
|
ValueTypeCode = FocasDataTypeCode.Float64,
|
|
});
|
|
MessagePackSerializer.Deserialize<double>(req.ValueBytes!).ShouldBe(3.14159);
|
|
|
|
var resp = RoundTrip(new WriteResponse { Success = true, StatusCode = 0 });
|
|
resp.Success.ShouldBeTrue();
|
|
resp.StatusCode.ShouldBe(0u);
|
|
}
|
|
|
|
[Fact]
|
|
public void PmcBitWriteRequest_preserves_bit_and_value()
|
|
{
|
|
var req = RoundTrip(new PmcBitWriteRequest
|
|
{
|
|
SessionId = 7,
|
|
Address = new FocasAddressDto { Kind = 0, PmcLetter = "Y", Number = 12 },
|
|
BitIndex = 5,
|
|
Value = true,
|
|
});
|
|
req.BitIndex.ShouldBe(5);
|
|
req.Value.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void SubscribeRequest_round_trips_multiple_items()
|
|
{
|
|
var original = new SubscribeRequest
|
|
{
|
|
SessionId = 1,
|
|
SubscriptionId = 100,
|
|
IntervalMs = 250,
|
|
Items =
|
|
[
|
|
new() { MonitoredItemId = 1, Address = new() { Kind = 0, PmcLetter = "R", Number = 100 }, DataType = FocasDataTypeCode.Bit },
|
|
new() { MonitoredItemId = 2, Address = new() { Kind = 2, Number = 500 }, DataType = FocasDataTypeCode.Float64 },
|
|
],
|
|
};
|
|
var decoded = RoundTrip(original);
|
|
decoded.Items.Length.ShouldBe(2);
|
|
decoded.Items[0].MonitoredItemId.ShouldBe(1);
|
|
decoded.Items[0].Address.PmcLetter.ShouldBe("R");
|
|
decoded.Items[1].DataType.ShouldBe(FocasDataTypeCode.Float64);
|
|
}
|
|
|
|
[Fact]
|
|
public void SubscribeResponse_rejected_items_survive()
|
|
{
|
|
var decoded = RoundTrip(new SubscribeResponse
|
|
{
|
|
Success = true,
|
|
RejectedMonitoredItemIds = [2, 7],
|
|
});
|
|
decoded.RejectedMonitoredItemIds.ShouldBe([2, 7]);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnsubscribeRequest_round_trips()
|
|
{
|
|
var decoded = RoundTrip(new UnsubscribeRequest { SubscriptionId = 42 });
|
|
decoded.SubscriptionId.ShouldBe(42);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnDataChangeNotification_round_trips()
|
|
{
|
|
var original = new OnDataChangeNotification
|
|
{
|
|
SubscriptionId = 100,
|
|
Changes =
|
|
[
|
|
new()
|
|
{
|
|
MonitoredItemId = 1,
|
|
StatusCode = 0,
|
|
ValueBytes = MessagePackSerializer.Serialize(true),
|
|
ValueTypeCode = FocasDataTypeCode.Bit,
|
|
SourceTimestampUtcUnixMs = 1_700_000_000_000,
|
|
},
|
|
],
|
|
};
|
|
var decoded = RoundTrip(original);
|
|
decoded.Changes.Length.ShouldBe(1);
|
|
MessagePackSerializer.Deserialize<bool>(decoded.Changes[0].ValueBytes!).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProbeRequest_and_response_round_trip()
|
|
{
|
|
var req = RoundTrip(new ProbeRequest { SessionId = 1, TimeoutMs = 500 });
|
|
req.TimeoutMs.ShouldBe(500);
|
|
|
|
var resp = RoundTrip(new ProbeResponse { Healthy = true, ObservedAtUtcUnixMs = 1_700_000_000_000 });
|
|
resp.Healthy.ShouldBeTrue();
|
|
resp.ObservedAtUtcUnixMs.ShouldBe(1_700_000_000_000);
|
|
}
|
|
|
|
[Fact]
|
|
public void RuntimeStatusChangeNotification_round_trips()
|
|
{
|
|
var decoded = RoundTrip(new RuntimeStatusChangeNotification
|
|
{
|
|
SessionId = 5,
|
|
RuntimeStatus = "Stopped",
|
|
ObservedAtUtcUnixMs = 1_700_000_000_000,
|
|
});
|
|
decoded.RuntimeStatus.ShouldBe("Stopped");
|
|
}
|
|
|
|
[Fact]
|
|
public void RecycleHostRequest_and_response_round_trip()
|
|
{
|
|
var req = RoundTrip(new RecycleHostRequest { Kind = "Hard", Reason = "wedge-detected" });
|
|
req.Kind.ShouldBe("Hard");
|
|
req.Reason.ShouldBe("wedge-detected");
|
|
|
|
var resp = RoundTrip(new RecycleStatusResponse { Accepted = true, GraceSeconds = 20 });
|
|
resp.Accepted.ShouldBeTrue();
|
|
resp.GraceSeconds.ShouldBe(20);
|
|
}
|
|
}
|