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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.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);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Tests.Domain;
|
||||
|
||||
public class ScadaContractsTests
|
||||
{
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_Bool()
|
||||
{
|
||||
var tv = new TypedValue { BoolValue = true };
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.BoolValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_Int32()
|
||||
{
|
||||
var tv = new TypedValue { Int32Value = 42 };
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.Int32Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_Double()
|
||||
{
|
||||
var tv = new TypedValue { DoubleValue = 3.14 };
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.DoubleValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_String()
|
||||
{
|
||||
var tv = new TypedValue { StringValue = "hello" };
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.StringValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_None_WhenDefault()
|
||||
{
|
||||
var tv = new TypedValue();
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_Datetime()
|
||||
{
|
||||
var tv = new TypedValue { DatetimeValue = DateTime.UtcNow.Ticks };
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.DatetimeValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_BytesValue()
|
||||
{
|
||||
var tv = new TypedValue { BytesValue = new byte[] { 1, 2, 3 } };
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.BytesValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedValue_GetValueCase_ArrayValue()
|
||||
{
|
||||
var tv = new TypedValue
|
||||
{
|
||||
ArrayValue = new ArrayValue
|
||||
{
|
||||
FloatValues = new FloatArray { Values = { 1.0f, 2.0f } }
|
||||
}
|
||||
};
|
||||
tv.GetValueCase().Should().Be(TypedValueCase.ArrayValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualityCode_IsGood()
|
||||
{
|
||||
var qc = new QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" };
|
||||
qc.IsGood.Should().BeTrue();
|
||||
qc.IsBad.Should().BeFalse();
|
||||
qc.IsUncertain.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualityCode_IsBad()
|
||||
{
|
||||
var qc = new QualityCode { StatusCode = 0x80000000, SymbolicName = "Bad" };
|
||||
qc.IsGood.Should().BeFalse();
|
||||
qc.IsBad.Should().BeTrue();
|
||||
qc.IsUncertain.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualityCode_IsUncertain()
|
||||
{
|
||||
var qc = new QualityCode { StatusCode = 0x40900000, SymbolicName = "UncertainLastUsableValue" };
|
||||
qc.IsGood.Should().BeFalse();
|
||||
qc.IsBad.Should().BeFalse();
|
||||
qc.IsUncertain.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VtqMessage_DefaultProperties()
|
||||
{
|
||||
var vtq = new VtqMessage();
|
||||
vtq.Tag.Should().BeEmpty();
|
||||
vtq.Value.Should().BeNull();
|
||||
vtq.TimestampUtcTicks.Should().Be(0);
|
||||
vtq.Quality.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteBatchAndWaitRequest_FlagValue_IsTypedValue()
|
||||
{
|
||||
var req = new WriteBatchAndWaitRequest
|
||||
{
|
||||
SessionId = "abc",
|
||||
FlagTag = "Motor.Done",
|
||||
FlagValue = new TypedValue { BoolValue = true },
|
||||
TimeoutMs = 5000,
|
||||
PollIntervalMs = 100
|
||||
};
|
||||
req.FlagValue.Should().NotBeNull();
|
||||
req.FlagValue!.GetValueCase().Should().Be(TypedValueCase.BoolValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteItem_Value_IsTypedValue()
|
||||
{
|
||||
var item = new WriteItem
|
||||
{
|
||||
Tag = "Motor.Speed",
|
||||
Value = new TypedValue { DoubleValue = 42.5 }
|
||||
};
|
||||
item.Value.Should().NotBeNull();
|
||||
item.Value!.GetValueCase().Should().Be(TypedValueCase.DoubleValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Tests.Domain;
|
||||
|
||||
public class VtqTests
|
||||
{
|
||||
[Fact]
|
||||
public void Good_FactoryMethod()
|
||||
{
|
||||
var vtq = Vtq.Good(42.0);
|
||||
vtq.Value.Should().Be(42.0);
|
||||
vtq.Quality.Should().Be(Quality.Good);
|
||||
vtq.Timestamp.Kind.Should().Be(DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bad_FactoryMethod()
|
||||
{
|
||||
var vtq = Vtq.Bad();
|
||||
vtq.Value.Should().BeNull();
|
||||
vtq.Quality.Should().Be(Quality.Bad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Uncertain_FactoryMethod()
|
||||
{
|
||||
var vtq = Vtq.Uncertain("stale");
|
||||
vtq.Value.Should().Be("stale");
|
||||
vtq.Quality.Should().Be(Quality.Uncertain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>ZB.MOM.WW.LmxProxy.Client.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="7.2.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.68.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxProxy.Client\ZB.MOM.WW.LmxProxy.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Proto file for cross-stack serialization tests (Host proto → Client code-first) -->
|
||||
<ItemGroup>
|
||||
<Protobuf Include="..\..\src\ZB.MOM.WW.LmxProxy.Host\Grpc\Protos\scada.proto" GrpcServices="None" Link="Protos\scada.proto" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>ZB.MOM.WW.LmxProxy.Host.Tests</RootNamespace>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Platforms>x86</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.2" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxProxy.Host\ZB.MOM.WW.LmxProxy.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user