// xUnit1051: MessagePackSerializer.Serialize/Deserialize have optional CancellationToken // overloads; these are synchronous parity tests — suppressing the false-positive advisory. #pragma warning disable xUnit1051 using MessagePack; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests; /// /// Wire-parity tests for the client-side IPC contracts (Contracts.cs + Framing.cs). /// These tests pin the MessagePack byte representation of each DTO using known inputs /// and assert byte-equality against expected values. Because the sidecar (.NET 4.8) /// carries a byte-identical mirror of these DTOs, a silent [Key] index drift or /// field-type change in either copy would cause a mismatch here and be caught at build /// time — without needing to reference the net48 sidecar assembly from a net10 test /// project (which the TFM mismatch prevents). (Finding 009.) /// public sealed class ContractsWireParityTests { // ---- HistorianSampleDto ---- // Fields at Key(0)=ValueBytes(null), Key(1)=Quality(0), Key(2)=TimestampUtcTicks(0) // MessagePack fixarray(3) + nil + fixint(0) + fixint(0) = 93 c0 00 00 [Fact] public void HistorianSampleDto_SerializedBytes_AreStable() { var dto = new HistorianSampleDto { ValueBytes = null, Quality = 0, TimestampUtcTicks = 0 }; var bytes = MessagePackSerializer.Serialize(dto); // fixarray(3) = 0x93, nil = 0xC0, fixint(0) = 0x00, fixint(0) = 0x00 bytes.ShouldBe(new byte[] { 0x93, 0xC0, 0x00, 0x00 }); } [Fact] public void HistorianSampleDto_WithValue_RoundTrips() { var original = new HistorianSampleDto { ValueBytes = MessagePackSerializer.Serialize(42.5), Quality = 192, TimestampUtcTicks = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks, }; var bytes = MessagePackSerializer.Serialize(original); var roundTripped = MessagePackSerializer.Deserialize(bytes); roundTripped.Quality.ShouldBe((byte)192); roundTripped.TimestampUtcTicks.ShouldBe(original.TimestampUtcTicks); roundTripped.ValueBytes.ShouldBe(original.ValueBytes); } // ---- HistorianAggregateSampleDto ---- // Key(0)=Value(null), Key(1)=TimestampUtcTicks(0) // fixarray(2) + nil + fixint(0) = 92 c0 00 [Fact] public void HistorianAggregateSampleDto_SerializedBytes_AreStable() { var dto = new HistorianAggregateSampleDto { Value = null, TimestampUtcTicks = 0 }; var bytes = MessagePackSerializer.Serialize(dto); // fixarray(2) = 0x92, nil = 0xC0, fixint(0) = 0x00 bytes.ShouldBe(new byte[] { 0x92, 0xC0, 0x00 }); } // ---- ReadRawRequest ---- // 5 fields at Key(0..4). TagName="", StartUtcTicks=0, EndUtcTicks=0, MaxValues=0, CorrelationId="" // fixarray(5) + fixstr(0)="" + fixint(0) + fixint(0) + fixint(0) + fixstr(0)="" [Fact] public void ReadRawRequest_EmptyInstance_SerializesAsFixArray5() { var req = new ReadRawRequest(); var bytes = MessagePackSerializer.Serialize(req); // Should start with fixarray(5) = 0x95 bytes[0].ShouldBe((byte)0x95); // Round-trip verification var rt = MessagePackSerializer.Deserialize(bytes); rt.TagName.ShouldBe(string.Empty); rt.MaxValues.ShouldBe(0); } [Fact] public void ReadRawRequest_WithValues_RoundTrips() { var original = new ReadRawRequest { TagName = "Tank.Level", StartUtcTicks = 100L, EndUtcTicks = 200L, MaxValues = 500, CorrelationId = "abc", }; var bytes = MessagePackSerializer.Serialize(original); var rt = MessagePackSerializer.Deserialize(bytes); rt.TagName.ShouldBe("Tank.Level"); rt.StartUtcTicks.ShouldBe(100L); rt.EndUtcTicks.ShouldBe(200L); rt.MaxValues.ShouldBe(500); rt.CorrelationId.ShouldBe("abc"); } // ---- ReadRawReply ---- [Fact] public void ReadRawReply_RoundTrips() { var original = new ReadRawReply { CorrelationId = "x", Success = true, Error = null, Samples = [new HistorianSampleDto { Quality = 192, TimestampUtcTicks = 99L }], }; var bytes = MessagePackSerializer.Serialize(original); var rt = MessagePackSerializer.Deserialize(bytes); rt.CorrelationId.ShouldBe("x"); rt.Success.ShouldBeTrue(); rt.Error.ShouldBeNull(); rt.Samples.Length.ShouldBe(1); rt.Samples[0].Quality.ShouldBe((byte)192); rt.Samples[0].TimestampUtcTicks.ShouldBe(99L); } // ---- ReadAtTimeRequest / ReadAtTimeReply ---- [Fact] public void ReadAtTimeRequest_RoundTrips() { var ticks = new long[] { 100L, 200L, 300L }; var original = new ReadAtTimeRequest { TagName = "T", TimestampsUtcTicks = ticks, CorrelationId = "c" }; var bytes = MessagePackSerializer.Serialize(original); var rt = MessagePackSerializer.Deserialize(bytes); rt.TagName.ShouldBe("T"); rt.TimestampsUtcTicks.ShouldBe(ticks); rt.CorrelationId.ShouldBe("c"); } // ---- WriteAlarmEventsRequest / WriteAlarmEventsReply ---- [Fact] public void WriteAlarmEventsRequest_RoundTrips() { var original = new WriteAlarmEventsRequest { Events = [ new AlarmHistorianEventDto { EventId = "ev1", SourceName = "Tank/HiHi", ConditionId = "HiHi", AlarmType = "LimitAlarm:Activated", Message = "msg", Severity = 700, EventTimeUtcTicks = 999L, AckComment = null, }, ], CorrelationId = "r", }; var bytes = MessagePackSerializer.Serialize(original); var rt = MessagePackSerializer.Deserialize(bytes); rt.CorrelationId.ShouldBe("r"); rt.Events.Length.ShouldBe(1); rt.Events[0].EventId.ShouldBe("ev1"); rt.Events[0].SourceName.ShouldBe("Tank/HiHi"); rt.Events[0].Severity.ShouldBe((ushort)700); rt.Events[0].EventTimeUtcTicks.ShouldBe(999L); } [Fact] public void WriteAlarmEventsReply_RoundTrips() { var original = new WriteAlarmEventsReply { CorrelationId = "r", Success = true, Error = null, PerEventOk = [true, false, true], }; var bytes = MessagePackSerializer.Serialize(original); var rt = MessagePackSerializer.Deserialize(bytes); rt.CorrelationId.ShouldBe("r"); rt.Success.ShouldBeTrue(); rt.PerEventOk.ShouldBe(new[] { true, false, true }); } // ---- MessageKind enum values are pinned ---- // Changing a MessageKind value is a wire break; pin them explicitly. [Fact] public void MessageKind_Values_AreStable() { ((byte)MessageKind.Hello).ShouldBe((byte)0x01); ((byte)MessageKind.HelloAck).ShouldBe((byte)0x02); ((byte)MessageKind.ReadRawRequest).ShouldBe((byte)0x10); ((byte)MessageKind.ReadRawReply).ShouldBe((byte)0x11); ((byte)MessageKind.ReadProcessedRequest).ShouldBe((byte)0x12); ((byte)MessageKind.ReadProcessedReply).ShouldBe((byte)0x13); ((byte)MessageKind.ReadAtTimeRequest).ShouldBe((byte)0x14); ((byte)MessageKind.ReadAtTimeReply).ShouldBe((byte)0x15); ((byte)MessageKind.ReadEventsRequest).ShouldBe((byte)0x16); ((byte)MessageKind.ReadEventsReply).ShouldBe((byte)0x17); ((byte)MessageKind.WriteAlarmEventsRequest).ShouldBe((byte)0x20); ((byte)MessageKind.WriteAlarmEventsReply).ShouldBe((byte)0x21); } // ---- Framing constants are pinned ---- [Fact] public void Framing_Constants_AreStable() { Framing.LengthPrefixSize.ShouldBe(4); Framing.KindByteSize.ShouldBe(1); Framing.MaxFrameBodyBytes.ShouldBe(16 * 1024 * 1024); } }