Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/StatusCodeMapTests.cs
Joseph Doherty 85bdf0d58b PR 4.2 — IReadable abstraction + StatusCodeMap + MxValueDecoder
Read path scaffold + the byte→uint quality mapping table that the parity
matrix (PR 5.x) pins. PR 4.4 supplies the production GW-backed reader; this
PR ships the abstraction and the supporting infrastructure so 4.4 just
plugs the implementation in.

Files:
- Runtime/StatusCodeMap.cs — explicit OPC DA quality byte → OPC UA
  StatusCode uint mapping. Extends the legacy Galaxy.Host
  HistorianQualityMapper with named constants (Good / GoodLocalOverride,
  Uncertain + 4 substatuses, Bad + 7 substatuses, BadInternalError) and an
  MxStatusProxy → uint helper that honors success flag → detail byte →
  detected_by transport-error fallback. Unknown bytes fall back to category
  bucket with a once-per-session diagnostic log so field captures can
  extend the table.
- Runtime/MxValueDecoder.cs — gateway MxValue → boxed CLR value for the
  seven Galaxy data types (Boolean, Int32, Int64, Float32, Float64, String,
  DateTime) plus their array variants. Honors MxValue.IsNull and
  RawValue passthrough.
- Runtime/IGalaxyDataReader.cs — driver-side seam for one-shot reads. PR
  4.4 ships the production wrapper around MxGatewaySession.SubscribeBulk +
  StreamEvents + UnsubscribeBulk; this PR exposes the contract so
  GalaxyDriver.ReadAsync wires through it.
- Runtime/GalaxyMxSession.cs — wrapper around MxGatewaySession that owns
  the Register handle. ConnectAsync opens session + Register; AttachForTests
  lets tests bypass real gw construction. PR 4.3/4.4/4.5 add write,
  subscribe, and reconnect surfaces.

GalaxyDriver:
- Implements IReadable. ReadAsync routes through the injected
  IGalaxyDataReader (test seam) when present; production path throws
  NotSupportedException pointing at PR 4.4 — protects deployments running
  this PR from silent wrong reads while signaling that the legacy-host
  backend (Galaxy:Backend=legacy-host) handles reads in the meantime.
- Internal ctor extended with optional dataReader parameter (default null,
  preserves PR 4.0/4.1 callers).

Tests: 42 new — exhaustive byte→uint table for StatusCodeMap (15 known
codes + category-bucket fallback for unknowns + MxStatusProxy precedence
rules + OPC UA top-byte invariants), every MxValue oneof case for the
decoder (bool/int32/int64/float/double/string/timestamp/3 array variants/
raw bytes/null), GalaxyDriver IReadable wiring (route-through, empty-
request, no-reader-throws, post-dispose-throws, status-code preservation).
62 Galaxy tests total pass.

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

99 lines
4.0 KiB
C#

using MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
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);
}
[Fact]
public void FromMxStatus_NullStatus_IsGood()
{
StatusCodeMap.FromMxStatus(null).ShouldBe(StatusCodeMap.Good);
}
[Fact]
public void FromMxStatus_SuccessNonZero_IsGood()
{
var s = new MxStatusProxy { Success = 1 };
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Good);
}
[Fact]
public void FromMxStatus_SuccessZero_DetailKnown_MapsToSpecificCode()
{
var s = new MxStatusProxy { Success = 0, 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);
}
}