Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/StatusCodeMapTests.cs
Joseph Doherty 8fe7c8bea6 refactor(driver-galaxy): switch to sibling-repo MxGateway client + drop vendored libs
The sibling mxaccessgw repo (clients/dotnet/) restored a proper client
library + contracts under the new ZB.MOM.WW.MxGateway namespace, so the
binary-vendoring stopgap from PR Driver.Galaxy-016 can unwind via plan #1
of libs/README.md.

- csproj: replace <Reference HintPath="libs\MxGateway.*.dll"> with a
  ProjectReference into ..\..\..\..\mxaccessgw\clients\dotnet  ZB.MOM.WW.MxGateway.Client\. The five backfill PackageReference shims
  (Google.Protobuf, Grpc.Core.Api, Grpc.Net.Client, Polly.Core,
  Microsoft.Extensions.Logging.Abstractions) are now transitive again.
- Source: 'using MxGateway.X' -> 'using ZB.MOM.WW.MxGateway.X' across
  19 driver files + 14 test files. No fully-qualified MxGateway.* usages
  in code, so no behavioural changes — purely a using-prefix flip.
- libs/: deleted MxGateway.Client.dll, MxGateway.Contracts.dll, README.md
  (orphan after the unwind).

Verified: dotnet build clean (Release), all 245 Driver.Galaxy unit tests
pass, OtOpcUa service running with the new client DLL loaded
(opc.tcp://localhost:4840/OtOpcUa, no FileNotFound/TypeLoad/
MissingMethod in startup logs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:55:15 -04:00

141 lines
6.4 KiB
C#

using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
// MxStatusCategory needed for Driver.Galaxy-003 regression tests.
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
/// <summary>
/// Exhaustive table-driven tests for <see cref="StatusCodeMap"/>. Pinning the byte→uint
/// mapping here protects against accidental drift — every Galaxy deployment that
/// reaches the parity matrix in PR 5.2 depends on these specific OPC UA StatusCode
/// values matching the legacy <c>HistorianQualityMapper</c> output.
/// </summary>
public sealed class StatusCodeMapTests
{
[Theory]
[InlineData((byte)192, 0x00000000u)] // Good
[InlineData((byte)216, 0x00D80000u)] // Good_LocalOverride
[InlineData((byte)64, 0x40000000u)] // Uncertain
[InlineData((byte)68, 0x40A40000u)] // Uncertain_LastUsableValue
[InlineData((byte)80, 0x408D0000u)] // Uncertain_SensorNotAccurate
[InlineData((byte)84, 0x408E0000u)] // Uncertain_EngineeringUnitsExceeded
[InlineData((byte)88, 0x408F0000u)] // Uncertain_SubNormal
[InlineData((byte)0, 0x80000000u)] // Bad
[InlineData((byte)4, 0x80890000u)] // Bad_ConfigurationError
[InlineData((byte)8, 0x808A0000u)] // Bad_NotConnected
[InlineData((byte)12, 0x808B0000u)] // Bad_DeviceFailure
[InlineData((byte)16, 0x808C0000u)] // Bad_SensorFailure
[InlineData((byte)20, 0x80050000u)] // Bad_CommunicationError
[InlineData((byte)24, 0x808D0000u)] // Bad_OutOfService
[InlineData((byte)32, 0x80320000u)] // Bad_WaitingForInitialData
public void FromQualityByte_KnownValues_MapToOpcUaStatusCode(byte input, uint expected)
{
StatusCodeMap.FromQualityByte(input).ShouldBe(expected);
}
[Theory]
[InlineData((byte)200)] // Unknown Good — falls back to category bucket
[InlineData((byte)100)] // Unknown Uncertain
[InlineData((byte)40)] // Unknown Bad
public void FromQualityByte_UnknownValues_FallBackToCategoryBucket(byte input)
{
var mapped = StatusCodeMap.FromQualityByte(input);
if (input >= 192) mapped.ShouldBe(StatusCodeMap.Good);
else if (input >= 64) mapped.ShouldBe(StatusCodeMap.Uncertain);
else mapped.ShouldBe(StatusCodeMap.Bad);
}
// ===== Driver.Galaxy-003 regression: FromMxStatus uses IsSuccess() (category + success) =====
// The proto doc: "clients should branch on category, not on a specific success value."
// IsSuccess() requires BOTH success != 0 AND category == Ok — checking success alone
// would invert the mapping when the worker sets success=1 for an error code (the numeric
// value is NOT a boolean).
[Fact]
public void FromMxStatus_NullStatus_IsGood()
{
StatusCodeMap.FromMxStatus(null).ShouldBe(StatusCodeMap.Good);
}
[Fact]
public void FromMxStatus_SuccessNonZeroAndCategoryOk_IsGood()
{
// Both conditions required — this is the canonical OK path.
var s = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok };
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Good);
}
[Fact]
public void FromMxStatus_SuccessNonZeroButCategoryNotOk_IsNotGood()
{
// Bug fixed by Driver.Galaxy-003: success != 0 alone used to return Good even when
// category indicates an error. The proto says success is NOT a boolean — category wins.
var s = new MxStatusProxy { Success = 1, Category = MxStatusCategory.CommunicationError };
StatusCodeMap.FromMxStatus(s).ShouldNotBe(StatusCodeMap.Good);
}
[Fact]
public void FromMxStatus_SuccessZeroAndCategoryNotOk_DetailKnown_MapsToSpecificCode()
{
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.OperationalError, Detail = 8 /* Bad_NotConnected */ };
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadNotConnected);
}
[Fact]
public void FromMxStatus_SuccessZero_DetailZero_DetectedByNonZero_IsCommunicationError()
{
var s = new MxStatusProxy { Success = 0, Detail = 0, RawDetectedBy = 3 };
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadCommunicationError);
}
[Fact]
public void FromMxStatus_SuccessZero_AllZero_IsBad()
{
var s = new MxStatusProxy();
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
}
[Fact]
public void TopByteCategoryBits_StayWithinOpcUaConvention()
{
// Sanity check that every Bad code we mint actually has the Bad top byte (0x80…),
// every Uncertain has 0x40…, every Good has 0x00…. Pins the OPC UA Part 4 invariant.
StatusCodeMap.Good.ShouldBeLessThan(0x40000000u);
StatusCodeMap.GoodLocalOverride.ShouldBeLessThan(0x40000000u);
((StatusCodeMap.Uncertain >> 30) & 0x3u).ShouldBe(1u);
((StatusCodeMap.UncertainLastUsableValue >> 30) & 0x3u).ShouldBe(1u);
((StatusCodeMap.Bad >> 30) & 0x3u).ShouldBe(2u);
((StatusCodeMap.BadNotConnected >> 30) & 0x3u).ShouldBe(2u);
((StatusCodeMap.BadOutOfService >> 30) & 0x3u).ShouldBe(2u);
}
// ===== Driver.Galaxy-004 regression: ToQualityCategoryByte lives next to its inverse =====
[Theory]
[InlineData(0x00000000u, (byte)192)] // Good
[InlineData(0x00D80000u, (byte)192)] // GoodLocalOverride — still Good category
[InlineData(0x40000000u, (byte)64)] // Uncertain
[InlineData(0x408F0000u, (byte)64)] // UncertainSubNormal — still Uncertain category
[InlineData(0x80000000u, (byte)0)] // Bad
[InlineData(0x808A0000u, (byte)0)] // BadNotConnected — still Bad category
[InlineData(0x80020000u, (byte)0)] // BadInternalError — still Bad category
public void ToQualityCategoryByte_ExtractsTopTwoBitsAsOpcDaByte(uint statusCode, byte expected)
{
StatusCodeMap.ToQualityCategoryByte(statusCode).ShouldBe(expected);
}
[Fact]
public void ToQualityCategoryByte_IsRightInverseOfFromQualityByte_ForCategoryBytes()
{
// The category bytes the probe watcher uses are 0, 64, 192. Round-trip: a value
// that came FROM those bytes should map back to the same byte.
StatusCodeMap.ToQualityCategoryByte(StatusCodeMap.FromQualityByte(0)).ShouldBe((byte)0);
StatusCodeMap.ToQualityCategoryByte(StatusCodeMap.FromQualityByte(64)).ShouldBe((byte)64);
StatusCodeMap.ToQualityCategoryByte(StatusCodeMap.FromQualityByte(192)).ShouldBe((byte)192);
}
}