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>
106 lines
3.9 KiB
C#
106 lines
3.9 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="GalaxyDriver"/>'s <c>IReadable</c> wiring. PR 4.2 ships the
|
|
/// abstraction (<see cref="IGalaxyDataReader"/>) and the wiring; PR 4.4 supplies the
|
|
/// production gateway-backed reader. These tests verify the wiring against a fake
|
|
/// reader plus the explicit "no reader → NotSupportedException" fallback that protects
|
|
/// deployments running on this PR from silently producing wrong reads.
|
|
/// </summary>
|
|
public sealed class GalaxyDriverReadTests
|
|
{
|
|
private static GalaxyDriverOptions Opts() => new(
|
|
new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
|
|
new GalaxyMxAccessOptions("OtOpcUa-A"),
|
|
new GalaxyRepositoryOptions(),
|
|
new GalaxyReconnectOptions());
|
|
|
|
private sealed class FakeReader : IGalaxyDataReader
|
|
{
|
|
public IReadOnlyList<string>? LastRequest { get; private set; }
|
|
public Func<IReadOnlyList<string>, IReadOnlyList<DataValueSnapshot>> Decide { get; set; } =
|
|
tags => tags.Select(t => new DataValueSnapshot(
|
|
Value: t,
|
|
StatusCode: StatusCodeMap.Good,
|
|
SourceTimestampUtc: DateTime.UtcNow,
|
|
ServerTimestampUtc: DateTime.UtcNow)).ToArray();
|
|
|
|
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
|
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
|
{
|
|
LastRequest = fullReferences;
|
|
return Task.FromResult(Decide(fullReferences));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_RoutesThroughInjectedReader()
|
|
{
|
|
var reader = new FakeReader();
|
|
var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: reader);
|
|
|
|
var result = await driver.ReadAsync(["Tank1.Level", "Tank2.Level"], CancellationToken.None);
|
|
|
|
reader.LastRequest.ShouldBe(new[] { "Tank1.Level", "Tank2.Level" });
|
|
result.Count.ShouldBe(2);
|
|
result[0].Value.ShouldBe("Tank1.Level");
|
|
result[0].StatusCode.ShouldBe(StatusCodeMap.Good);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_EmptyRequest_ReturnsEmpty_WithoutCallingReader()
|
|
{
|
|
var reader = new FakeReader();
|
|
var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: reader);
|
|
|
|
var result = await driver.ReadAsync([], CancellationToken.None);
|
|
|
|
result.ShouldBeEmpty();
|
|
reader.LastRequest.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_NoReader_Throws_PointingAtPR44()
|
|
{
|
|
var driver = new GalaxyDriver("g", Opts());
|
|
|
|
var ex = await Should.ThrowAsync<NotSupportedException>(() =>
|
|
driver.ReadAsync(["x"], CancellationToken.None));
|
|
ex.Message.ShouldContain("PR 4.4");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_AfterDispose_Throws()
|
|
{
|
|
var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: new FakeReader());
|
|
driver.Dispose();
|
|
await Should.ThrowAsync<ObjectDisposedException>(() =>
|
|
driver.ReadAsync(["x"], CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_PreservesReaderStatusCodes()
|
|
{
|
|
var reader = new FakeReader
|
|
{
|
|
Decide = tags => new DataValueSnapshot[]
|
|
{
|
|
new(42.0, StatusCodeMap.Good, DateTime.UtcNow, DateTime.UtcNow),
|
|
new(null, StatusCodeMap.BadNotConnected, null, DateTime.UtcNow),
|
|
},
|
|
};
|
|
var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: reader);
|
|
|
|
var result = await driver.ReadAsync(["a", "b"], CancellationToken.None);
|
|
|
|
result[0].StatusCode.ShouldBe(StatusCodeMap.Good);
|
|
result[1].StatusCode.ShouldBe(StatusCodeMap.BadNotConnected);
|
|
}
|
|
}
|