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:
Joseph Doherty
2026-03-21 23:41:56 -04:00
parent 08d2a07d8b
commit 0d63fb1105
87 changed files with 3389 additions and 956 deletions

View File

@@ -0,0 +1,87 @@
using FluentAssertions;
using Xunit;
using ZB.MOM.WW.LmxProxy.Host.Domain;
namespace ZB.MOM.WW.LmxProxy.Host.Tests.Domain
{
public class QualityCodeMapperTests
{
[Theory]
[InlineData(Quality.Good, 0x00000000u, "Good")]
[InlineData(Quality.Good_LocalOverride, 0x00D80000u, "GoodLocalOverride")]
[InlineData(Quality.Bad, 0x80000000u, "Bad")]
[InlineData(Quality.Bad_ConfigError, 0x80040000u, "BadConfigurationError")]
[InlineData(Quality.Bad_NotConnected, 0x808A0000u, "BadNotConnected")]
[InlineData(Quality.Bad_DeviceFailure, 0x806B0000u, "BadDeviceFailure")]
[InlineData(Quality.Bad_SensorFailure, 0x806D0000u, "BadSensorFailure")]
[InlineData(Quality.Bad_CommFailure, 0x80050000u, "BadCommunicationFailure")]
[InlineData(Quality.Bad_OutOfService, 0x808F0000u, "BadOutOfService")]
[InlineData(Quality.Bad_WaitingForInitialData, 0x80320000u, "BadWaitingForInitialData")]
[InlineData(Quality.Uncertain_LastUsable, 0x40900000u, "UncertainLastUsableValue")]
[InlineData(Quality.Uncertain_SensorNotAcc, 0x42390000u, "UncertainSensorNotAccurate")]
[InlineData(Quality.Uncertain_EuExceeded, 0x40540000u, "UncertainEngineeringUnitsExceeded")]
[InlineData(Quality.Uncertain_SubNormal, 0x40580000u, "UncertainSubNormal")]
public void ToQualityCode_MapsCorrectly(Quality quality, uint expectedStatusCode, string expectedName)
{
var qc = QualityCodeMapper.ToQualityCode(quality);
qc.StatusCode.Should().Be(expectedStatusCode);
qc.SymbolicName.Should().Be(expectedName);
}
[Theory]
[InlineData(0x00000000u, Quality.Good)]
[InlineData(0x80000000u, Quality.Bad)]
[InlineData(0x80040000u, Quality.Bad_ConfigError)]
[InlineData(0x806D0000u, Quality.Bad_SensorFailure)]
[InlineData(0x40900000u, Quality.Uncertain_LastUsable)]
public void FromStatusCode_MapsCorrectly(uint statusCode, Quality expectedQuality)
{
QualityCodeMapper.FromStatusCode(statusCode).Should().Be(expectedQuality);
}
[Fact]
public void FromStatusCode_UnknownGoodCode_FallsBackToGood()
{
QualityCodeMapper.FromStatusCode(0x00FF0000).Should().Be(Quality.Good);
}
[Fact]
public void FromStatusCode_UnknownBadCode_FallsBackToBad()
{
QualityCodeMapper.FromStatusCode(0x80FF0000).Should().Be(Quality.Bad);
}
[Fact]
public void FromStatusCode_UnknownUncertainCode_FallsBackToUncertain()
{
QualityCodeMapper.FromStatusCode(0x40FF0000).Should().Be(Quality.Uncertain);
}
[Theory]
[InlineData(0x00000000u, "Good")]
[InlineData(0x80000000u, "Bad")]
[InlineData(0x806D0000u, "BadSensorFailure")]
[InlineData(0x40900000u, "UncertainLastUsableValue")]
[InlineData(0x80FF0000u, "Bad")] // unknown bad code falls back
public void GetSymbolicName_ReturnsCorrectName(uint statusCode, string expectedName)
{
QualityCodeMapper.GetSymbolicName(statusCode).Should().Be(expectedName);
}
[Fact]
public void FactoryMethods_ReturnCorrectCodes()
{
QualityCodeMapper.Good().StatusCode.Should().Be(0x00000000u);
QualityCodeMapper.Bad().StatusCode.Should().Be(0x80000000u);
QualityCodeMapper.BadConfigurationError().StatusCode.Should().Be(0x80040000u);
QualityCodeMapper.BadCommunicationFailure().StatusCode.Should().Be(0x80050000u);
QualityCodeMapper.BadNotConnected().StatusCode.Should().Be(0x808A0000u);
QualityCodeMapper.BadDeviceFailure().StatusCode.Should().Be(0x806B0000u);
QualityCodeMapper.BadSensorFailure().StatusCode.Should().Be(0x806D0000u);
QualityCodeMapper.BadOutOfService().StatusCode.Should().Be(0x808F0000u);
QualityCodeMapper.BadWaitingForInitialData().StatusCode.Should().Be(0x80320000u);
QualityCodeMapper.GoodLocalOverride().StatusCode.Should().Be(0x00D80000u);
QualityCodeMapper.UncertainLastUsableValue().StatusCode.Should().Be(0x40900000u);
}
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
using Xunit;
using ZB.MOM.WW.LmxProxy.Host.Domain;
namespace ZB.MOM.WW.LmxProxy.Host.Tests.Domain
{
public class QualityExtensionsTests
{
[Theory]
[InlineData(Quality.Good, true)]
[InlineData(Quality.Good_LocalOverride, true)]
[InlineData(Quality.Uncertain, false)]
[InlineData(Quality.Bad, false)]
public void IsGood(Quality q, bool expected)
{
q.IsGood().Should().Be(expected);
}
[Theory]
[InlineData(Quality.Uncertain, true)]
[InlineData(Quality.Uncertain_LastUsable, true)]
[InlineData(Quality.Good, false)]
[InlineData(Quality.Bad, false)]
public void IsUncertain(Quality q, bool expected)
{
q.IsUncertain().Should().Be(expected);
}
[Theory]
[InlineData(Quality.Bad, true)]
[InlineData(Quality.Bad_CommFailure, true)]
[InlineData(Quality.Good, false)]
[InlineData(Quality.Uncertain, false)]
public void IsBad(Quality q, bool expected)
{
q.IsBad().Should().Be(expected);
}
}
}

View File

@@ -0,0 +1,196 @@
using System;
using FluentAssertions;
using Xunit;
using ZB.MOM.WW.LmxProxy.Host.Domain;
namespace ZB.MOM.WW.LmxProxy.Host.Tests.Domain
{
public class TypedValueConverterTests
{
[Fact]
public void Null_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue(null);
tv.Should().BeNull();
TypedValueConverter.FromTypedValue(null).Should().BeNull();
}
[Fact]
public void DBNull_MapsToNull()
{
var tv = TypedValueConverter.ToTypedValue(DBNull.Value);
tv.Should().BeNull();
}
[Fact]
public void Bool_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue(true);
tv.Should().NotBeNull();
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.BoolValue);
tv.BoolValue.Should().BeTrue();
TypedValueConverter.FromTypedValue(tv).Should().Be(true);
var tvFalse = TypedValueConverter.ToTypedValue(false);
tvFalse!.BoolValue.Should().BeFalse();
TypedValueConverter.FromTypedValue(tvFalse).Should().Be(false);
}
[Fact]
public void Short_WidensToInt32()
{
var tv = TypedValueConverter.ToTypedValue((short)42);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.Int32Value);
tv.Int32Value.Should().Be(42);
TypedValueConverter.FromTypedValue(tv).Should().Be(42);
}
[Fact]
public void Int_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue(int.MaxValue);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.Int32Value);
tv.Int32Value.Should().Be(int.MaxValue);
TypedValueConverter.FromTypedValue(tv).Should().Be(int.MaxValue);
}
[Fact]
public void Long_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue(long.MaxValue);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.Int64Value);
tv.Int64Value.Should().Be(long.MaxValue);
TypedValueConverter.FromTypedValue(tv).Should().Be(long.MaxValue);
}
[Fact]
public void UShort_WidensToInt32()
{
var tv = TypedValueConverter.ToTypedValue((ushort)65535);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.Int32Value);
tv.Int32Value.Should().Be(65535);
}
[Fact]
public void UInt_WidensToInt64()
{
var tv = TypedValueConverter.ToTypedValue(uint.MaxValue);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.Int64Value);
tv.Int64Value.Should().Be(uint.MaxValue);
}
[Fact]
public void ULong_MapsToInt64()
{
var tv = TypedValueConverter.ToTypedValue((ulong)12345678);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.Int64Value);
tv.Int64Value.Should().Be(12345678);
}
[Fact]
public void Float_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue(3.14159f);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.FloatValue);
tv.FloatValue.Should().Be(3.14159f);
TypedValueConverter.FromTypedValue(tv).Should().Be(3.14159f);
}
[Fact]
public void Double_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue(2.718281828459045);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.DoubleValue);
tv.DoubleValue.Should().Be(2.718281828459045);
TypedValueConverter.FromTypedValue(tv).Should().Be(2.718281828459045);
}
[Fact]
public void String_RoundTrips()
{
var tv = TypedValueConverter.ToTypedValue("Hello World");
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.StringValue);
tv.StringValue.Should().Be("Hello World");
TypedValueConverter.FromTypedValue(tv).Should().Be("Hello World");
}
[Fact]
public void DateTime_RoundTrips_AsUtcTicks()
{
var dt = new DateTime(2026, 3, 21, 12, 0, 0, DateTimeKind.Utc);
var tv = TypedValueConverter.ToTypedValue(dt);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.DatetimeValue);
tv.DatetimeValue.Should().Be(dt.Ticks);
var result = (DateTime)TypedValueConverter.FromTypedValue(tv)!;
result.Kind.Should().Be(DateTimeKind.Utc);
result.Ticks.Should().Be(dt.Ticks);
}
[Fact]
public void ByteArray_RoundTrips()
{
var bytes = new byte[] { 0x00, 0xFF, 0x42 };
var tv = TypedValueConverter.ToTypedValue(bytes);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.BytesValue);
var result = (byte[])TypedValueConverter.FromTypedValue(tv)!;
result.Should().BeEquivalentTo(bytes);
}
[Fact]
public void Decimal_MapsToDouble()
{
var tv = TypedValueConverter.ToTypedValue(123.456m);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.DoubleValue);
tv.DoubleValue.Should().BeApproximately(123.456, 0.001);
}
[Fact]
public void FloatArray_RoundTrips()
{
var arr = new float[] { 1.0f, 2.0f, 3.0f };
var tv = TypedValueConverter.ToTypedValue(arr);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.ArrayValue);
var result = (float[])TypedValueConverter.FromTypedValue(tv)!;
result.Should().BeEquivalentTo(arr);
}
[Fact]
public void IntArray_RoundTrips()
{
var arr = new int[] { 10, 20, 30 };
var tv = TypedValueConverter.ToTypedValue(arr);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.ArrayValue);
var result = (int[])TypedValueConverter.FromTypedValue(tv)!;
result.Should().BeEquivalentTo(arr);
}
[Fact]
public void StringArray_RoundTrips()
{
var arr = new string[] { "a", "b", "c" };
var tv = TypedValueConverter.ToTypedValue(arr);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.ArrayValue);
var result = (string[])TypedValueConverter.FromTypedValue(tv)!;
result.Should().BeEquivalentTo(arr);
}
[Fact]
public void DoubleArray_RoundTrips()
{
var arr = new double[] { 1.1, 2.2, 3.3 };
var tv = TypedValueConverter.ToTypedValue(arr);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.ArrayValue);
var result = (double[])TypedValueConverter.FromTypedValue(tv)!;
result.Should().BeEquivalentTo(arr);
}
[Fact]
public void UnrecognizedType_FallsBackToString()
{
var guid = Guid.NewGuid();
var tv = TypedValueConverter.ToTypedValue(guid);
tv!.ValueCase.Should().Be(Scada.TypedValue.ValueOneofCase.StringValue);
tv.StringValue.Should().Be(guid.ToString());
}
}
}