feat(lmxproxy): phase 1 — v2 protocol types and domain model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
using Google.Protobuf;
|
||||
using ProtoBuf;
|
||||
using Xunit;
|
||||
using ProtoGenerated = Scada;
|
||||
using CodeFirst = ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Tests.CrossStack;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies wire compatibility between Host proto-generated types and Client code-first types.
|
||||
/// Serializes with one stack, deserializes with the other.
|
||||
/// </summary>
|
||||
public class CrossStackSerializationTests
|
||||
{
|
||||
// ── Proto-generated → Code-first ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_BoolValue()
|
||||
{
|
||||
// Arrange: proto-generated VtqMessage with bool TypedValue
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Running",
|
||||
Value = new ProtoGenerated.TypedValue { BoolValue = true },
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" }
|
||||
};
|
||||
|
||||
// Act: serialize with proto, deserialize with protobuf-net
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
// Assert
|
||||
codeFirst.Should().NotBeNull();
|
||||
codeFirst.Tag.Should().Be("Motor.Running");
|
||||
codeFirst.Value.Should().NotBeNull();
|
||||
codeFirst.Value!.BoolValue.Should().BeTrue();
|
||||
codeFirst.TimestampUtcTicks.Should().Be(638789000000000000L);
|
||||
codeFirst.Quality.Should().NotBeNull();
|
||||
codeFirst.Quality!.StatusCode.Should().Be(0x00000000u);
|
||||
codeFirst.Quality.SymbolicName.Should().Be("Good");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_DoubleValue()
|
||||
{
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Speed",
|
||||
Value = new ProtoGenerated.TypedValue { DoubleValue = 42.5 },
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" }
|
||||
};
|
||||
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
codeFirst.Value.Should().NotBeNull();
|
||||
codeFirst.Value!.DoubleValue.Should().Be(42.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_StringValue()
|
||||
{
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Name",
|
||||
Value = new ProtoGenerated.TypedValue { StringValue = "Pump A" },
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" }
|
||||
};
|
||||
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
codeFirst.Value.Should().NotBeNull();
|
||||
codeFirst.Value!.StringValue.Should().Be("Pump A");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_Int32Value()
|
||||
{
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Count",
|
||||
Value = new ProtoGenerated.TypedValue { Int32Value = 2147483647 },
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" }
|
||||
};
|
||||
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
codeFirst.Value!.Int32Value.Should().Be(int.MaxValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_BadQuality()
|
||||
{
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Fault",
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x806D0000, SymbolicName = "BadSensorFailure" }
|
||||
};
|
||||
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
codeFirst.Quality!.StatusCode.Should().Be(0x806D0000u);
|
||||
codeFirst.Quality.SymbolicName.Should().Be("BadSensorFailure");
|
||||
codeFirst.Quality.IsBad.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_NullValue()
|
||||
{
|
||||
// No Value field set — represents null
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Optional",
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x80000000, SymbolicName = "Bad" }
|
||||
};
|
||||
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
// When no oneof is set, the Value object may be null or all-default
|
||||
// Either way, GetValueCase() should return None
|
||||
if (codeFirst.Value != null)
|
||||
codeFirst.Value.GetValueCase().Should().Be(CodeFirst.TypedValueCase.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_ProtoToCodeFirst_FloatArrayValue()
|
||||
{
|
||||
var floatArr = new ProtoGenerated.FloatArray();
|
||||
floatArr.Values.AddRange(new[] { 1.0f, 2.0f, 3.0f });
|
||||
var protoMsg = new ProtoGenerated.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Samples",
|
||||
Value = new ProtoGenerated.TypedValue
|
||||
{
|
||||
ArrayValue = new ProtoGenerated.ArrayValue { FloatValues = floatArr }
|
||||
},
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" }
|
||||
};
|
||||
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.VtqMessage>(new MemoryStream(bytes));
|
||||
|
||||
codeFirst.Value.Should().NotBeNull();
|
||||
codeFirst.Value!.ArrayValue.Should().NotBeNull();
|
||||
codeFirst.Value.ArrayValue!.FloatValues.Should().NotBeNull();
|
||||
codeFirst.Value.ArrayValue.FloatValues!.Values.Should().BeEquivalentTo(new[] { 1.0f, 2.0f, 3.0f });
|
||||
}
|
||||
|
||||
// ── Code-first → Proto-generated ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_CodeFirstToProto_DoubleValue()
|
||||
{
|
||||
var codeFirst = new CodeFirst.VtqMessage
|
||||
{
|
||||
Tag = "Motor.Speed",
|
||||
Value = new CodeFirst.TypedValue { DoubleValue = 99.9 },
|
||||
TimestampUtcTicks = 638789000000000000L,
|
||||
Quality = new CodeFirst.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" }
|
||||
};
|
||||
|
||||
// Serialize with protobuf-net
|
||||
var ms = new MemoryStream();
|
||||
Serializer.Serialize(ms, codeFirst);
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
// Deserialize with Google.Protobuf
|
||||
var protoMsg = ProtoGenerated.VtqMessage.Parser.ParseFrom(bytes);
|
||||
|
||||
protoMsg.Tag.Should().Be("Motor.Speed");
|
||||
protoMsg.Value.Should().NotBeNull();
|
||||
protoMsg.Value.DoubleValue.Should().Be(99.9);
|
||||
protoMsg.TimestampUtcTicks.Should().Be(638789000000000000L);
|
||||
protoMsg.Quality.StatusCode.Should().Be(0x00000000u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteRequest_CodeFirstToProto()
|
||||
{
|
||||
var codeFirst = new CodeFirst.WriteRequest
|
||||
{
|
||||
SessionId = "abc123",
|
||||
Tag = "Motor.Speed",
|
||||
Value = new CodeFirst.TypedValue { DoubleValue = 42.5 }
|
||||
};
|
||||
|
||||
var ms = new MemoryStream();
|
||||
Serializer.Serialize(ms, codeFirst);
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
var protoMsg = ProtoGenerated.WriteRequest.Parser.ParseFrom(bytes);
|
||||
protoMsg.SessionId.Should().Be("abc123");
|
||||
protoMsg.Tag.Should().Be("Motor.Speed");
|
||||
protoMsg.Value.Should().NotBeNull();
|
||||
protoMsg.Value.DoubleValue.Should().Be(42.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectRequest_RoundTrips()
|
||||
{
|
||||
var codeFirst = new CodeFirst.ConnectRequest { ClientId = "ScadaLink-1", ApiKey = "key-123" };
|
||||
var ms = new MemoryStream();
|
||||
Serializer.Serialize(ms, codeFirst);
|
||||
var protoMsg = ProtoGenerated.ConnectRequest.Parser.ParseFrom(ms.ToArray());
|
||||
protoMsg.ClientId.Should().Be("ScadaLink-1");
|
||||
protoMsg.ApiKey.Should().Be("key-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectResponse_RoundTrips()
|
||||
{
|
||||
var protoMsg = new ProtoGenerated.ConnectResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "Connected",
|
||||
SessionId = "abcdef1234567890abcdef1234567890"
|
||||
};
|
||||
var bytes = protoMsg.ToByteArray();
|
||||
var codeFirst = Serializer.Deserialize<CodeFirst.ConnectResponse>(new MemoryStream(bytes));
|
||||
codeFirst.Success.Should().BeTrue();
|
||||
codeFirst.Message.Should().Be("Connected");
|
||||
codeFirst.SessionId.Should().Be("abcdef1234567890abcdef1234567890");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteBatchAndWaitRequest_CodeFirstToProto_TypedFlagValue()
|
||||
{
|
||||
var codeFirst = new CodeFirst.WriteBatchAndWaitRequest
|
||||
{
|
||||
SessionId = "sess1",
|
||||
FlagTag = "Motor.Done",
|
||||
FlagValue = new CodeFirst.TypedValue { BoolValue = true },
|
||||
TimeoutMs = 5000,
|
||||
PollIntervalMs = 100,
|
||||
Items =
|
||||
{
|
||||
new CodeFirst.WriteItem
|
||||
{
|
||||
Tag = "Motor.Speed",
|
||||
Value = new CodeFirst.TypedValue { DoubleValue = 50.0 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var ms = new MemoryStream();
|
||||
Serializer.Serialize(ms, codeFirst);
|
||||
var protoMsg = ProtoGenerated.WriteBatchAndWaitRequest.Parser.ParseFrom(ms.ToArray());
|
||||
|
||||
protoMsg.FlagTag.Should().Be("Motor.Done");
|
||||
protoMsg.FlagValue.BoolValue.Should().BeTrue();
|
||||
protoMsg.TimeoutMs.Should().Be(5000);
|
||||
protoMsg.PollIntervalMs.Should().Be(100);
|
||||
protoMsg.Items.Should().HaveCount(1);
|
||||
protoMsg.Items[0].Tag.Should().Be("Motor.Speed");
|
||||
protoMsg.Items[0].Value.DoubleValue.Should().Be(50.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user