#519 — a driver's OPC UA status code now reaches a client with its sub-code intact. DriverInstanceActor projected the driver's uint onto a 3-state Good/Uncertain/Bad enum and OtOpcUaNodeManager re-expanded Bad to a bare 0x80000000, so BadNoCommunication, BadTypeMismatch and BadNotSupported were indistinguishable on the wire and every driver's status constants (incl. the 16 #497 had just corrected) were diagnostic in logs only. Resolved by option (b): the raw uint rides AttributeValuePublished -> AttributeValueUpdate -> IOpcUaAddressSpaceSink.WriteValue, OpcUaQuality is deleted, and severity classification is a helper (OpcUaStatus.IsGood/IsUncertain/IsBad). The surface was wider than the issue costed (41 files, not "the sink and 3 implementations"): OpcUaQuality had non-driver producers with no uint to carry. Each got its natural code — BadInternalError for a failed virtual-tag script, BadNoCommunication for driver comms loss — and ScriptedAlarmHostActor's pair of mutually-inverse uint<->enum mappers is deleted, having round-tripped a code the engine already held. AddressSpaceApplier's removal writes deliberately keep the generic Bad: not losing a code the driver had is this issue; inventing one for the deploy path is a separate call. Behaviour change: WriteAlarmQuality fires its Part 9 event on a status-code change rather than a severity-bucket change. While the annotation was 3-state those were the same predicate; now a move between two Bad reasons changes what a subscriber reads. New SDK-free named constants live on Commons.OpcUa.OpcUaStatus, and StatusCodeParityTests is widened to cover them — that guard is reflection-driven, so it can only see a named constant, never an inline literal. #517 — the symptom is real but the issue named the wrong path and its fix was unsafe. ShutdownAsync cannot race: DriverInstanceActor calls it from PostStop, after the mailbox drains. (Core/Hosting/DriverHost, the one class with a concurrent GetHealth, has no production call site.) InitializeAsync can: it runs on a fire-and-forget Task.Run, and MTConnectDriver writes Healthy then starts loops that can throw into a catch which disposes the client before writing Faulted. Moving the write earlier, as suggested, inverts the terminal state — teardown drains in-flight loops that write health as they unwind, so a late loop could leave Healthy at rest with every connection closed. The fix is a bracket (conservative before, terminal after), applied to MTConnect's init-failure path only; shutdown keeps its single trailing write, and the rule plus the reachability table are documented on IDriver.ShutdownAsync. CalculationDriver.GetHealth returned a hard-coded Healthy that consulted nothing — Healthy before initialize, after shutdown and after dispose. Now derived from _running. Its test, named Health_is_always_Connected, asserted the defect and is replaced. Every new guard was falsified by breaking the production code and watching it go red. The two sub-code tests each assert two different Bad codes, since a single-code assertion could pass against a hard-coded constant under the old projection. Pre-existing failures, verified unchanged on a stashed tree and not chased: 4 in Driver.AbLegacy.IntegrationTests (slc500/micrologix/plc5 fixtures not up) and 3 in Host.IntegrationTests. Closes #517 Closes #519 Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
+4
-4
@@ -65,8 +65,8 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
MaterialiseDualTree(sink);
|
||||
// One driver publish per NodeId (the fan-out the DriverHostActor performs) — identical payload.
|
||||
var ts = DateTime.UtcNow;
|
||||
sink.WriteValue(RawTagPath, 42.5f, OpcUaQuality.Good, ts, AddressSpaceRealm.Raw);
|
||||
sink.WriteValue(UnsVarPath, 42.5f, OpcUaQuality.Good, ts, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(RawTagPath, 42.5f, OpcUaStatus.Good, ts, AddressSpaceRealm.Raw);
|
||||
sink.WriteValue(UnsVarPath, 42.5f, OpcUaStatus.Good, ts, AddressSpaceRealm.Uns);
|
||||
|
||||
using var session = await OpenSessionAsync($"opc.tcp://127.0.0.1:{port}/OtOpcUa", ct);
|
||||
|
||||
@@ -125,8 +125,8 @@ public sealed class DualNamespaceAddressSpaceTests
|
||||
// The single source (one RawPath) fans to the raw NodeId AND every referencing UNS NodeId with
|
||||
// identical value/quality/timestamp — the fan-out drift invariant.
|
||||
var ts = new DateTime(2026, 7, 16, 8, 30, 0, DateTimeKind.Utc);
|
||||
sink.WriteValue(RawTagPath, 123.75f, OpcUaQuality.Good, ts, AddressSpaceRealm.Raw);
|
||||
sink.WriteValue(UnsVarPath, 123.75f, OpcUaQuality.Good, ts, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(RawTagPath, 123.75f, OpcUaStatus.Good, ts, AddressSpaceRealm.Raw);
|
||||
sink.WriteValue(UnsVarPath, 123.75f, OpcUaStatus.Good, ts, AddressSpaceRealm.Uns);
|
||||
|
||||
var rawValue = await session.ReadValueAsync(new NodeId(RawTagPath, rawNs), ct);
|
||||
var unsValue = await session.ReadValueAsync(new NodeId(UnsVarPath, unsNs), ct);
|
||||
|
||||
+5
-2
@@ -216,13 +216,13 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
|
||||
// Device goes comms-lost (connectivity path) → the next condition event carries a Bad Quality, while
|
||||
// the alarm stays active (annotation only — not asserted here, covered by the node-level test).
|
||||
sink.WriteAlarmQuality(RawAlarmPath, OpcUaQuality.Bad, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
sink.WriteAlarmQuality(RawAlarmPath, OpcUaStatus.Bad, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
await WaitUntilAsync(() => collector.Fields.Count >= 2, TimeSpan.FromSeconds(5));
|
||||
StatusCode.IsBad((StatusCode)collector.Fields[^1][QualityIndex].Value)
|
||||
.ShouldBeTrue("a comms-lost source's condition must report non-Good quality on the wire");
|
||||
|
||||
// Device recovers → Quality returns to Good.
|
||||
sink.WriteAlarmQuality(RawAlarmPath, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
sink.WriteAlarmQuality(RawAlarmPath, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
await WaitUntilAsync(() => collector.Fields.Count >= 3, TimeSpan.FromSeconds(5));
|
||||
StatusCode.IsGood((StatusCode)collector.Fields[^1][QualityIndex].Value)
|
||||
.ShouldBeTrue("a recovered source's condition reports Good quality again");
|
||||
@@ -294,6 +294,9 @@ public sealed class NativeAlarmEventIdentityFieldDeliveryTests
|
||||
private static EventFilter BuildQualityEventFilter()
|
||||
{
|
||||
var filter = new EventFilter();
|
||||
// BrowseNames.Quality is the SDK's Part 9 ConditionType.Quality browse name — NOT our snapshot's
|
||||
// renamed StatusCode member. Selecting the wrong clause here yields a null field and an NRE at the
|
||||
// cast, rather than a readable assertion failure.
|
||||
filter.AddSelectClause(ObjectTypes.ConditionType, BrowseNames.Quality);
|
||||
filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.Message);
|
||||
return filter;
|
||||
|
||||
+9
-9
@@ -67,7 +67,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
applier.MaterialiseHierarchy(withA);
|
||||
applier.MaterialiseEquipmentTags(withA);
|
||||
var nodeIdAString = V3NodeIds.Uns("eq-1", "A");
|
||||
sink.WriteValue(nodeIdAString, 41, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdAString, 41, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
// --- Open a client session + subscription + monitored item on A. ---
|
||||
using var session = await OpenSessionAsync($"opc.tcp://127.0.0.1:{port}/OtOpcUa", ct);
|
||||
@@ -116,7 +116,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
|
||||
// --- A's monitored item must still be alive: a new write to A is delivered. ---
|
||||
lock (gate) received.Clear();
|
||||
sink.WriteValue(nodeIdAString, 42, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdAString, 42, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
await WaitUntilAsync(() => { lock (gate) return received.Any(v => Equals(v.Value, 42)); }, TimeSpan.FromSeconds(5));
|
||||
lock (gate)
|
||||
@@ -127,7 +127,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
|
||||
// --- B is browsable + readable (the add landed). ---
|
||||
var nodeIdB = new NodeId(V3NodeIds.Uns("eq-1", "B"), ns);
|
||||
sink.WriteValue(nodeIdB.Identifier.ToString()!, 7, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdB.Identifier.ToString()!, 7, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
var bValue = await session.ReadValueAsync(nodeIdB, ct);
|
||||
bValue.StatusCode.ShouldNotBe(StatusCodes.BadNodeIdUnknown);
|
||||
bValue.Value.ShouldBe(7);
|
||||
@@ -176,8 +176,8 @@ public sealed class SubscriptionSurvivalTests
|
||||
applier.MaterialiseEquipmentTags(withAB);
|
||||
var nodeIdAString = V3NodeIds.Uns("eq-1", "A");
|
||||
var nodeIdBString = V3NodeIds.Uns("eq-1", "B");
|
||||
sink.WriteValue(nodeIdAString, 10, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdBString, 20, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdAString, 10, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdBString, 20, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
using var session = await OpenSessionAsync($"opc.tcp://127.0.0.1:{port}/OtOpcUa", ct);
|
||||
var ns = (ushort)session.NamespaceUris.GetIndex(OtOpcUaNodeManager.DefaultNamespaceUri);
|
||||
@@ -213,7 +213,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
await WaitUntilAsync(() => { lock (gate) return receivedB.Any(v => StatusCode.IsBad(v.StatusCode)); }, TimeSpan.FromSeconds(5));
|
||||
|
||||
// A stays alive: a fresh write to A is delivered.
|
||||
sink.WriteValue(nodeIdAString, 11, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdAString, 11, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
await WaitUntilAsync(() => { lock (gate) return receivedA.Any(v => Equals(v.Value, 11)); }, TimeSpan.FromSeconds(5));
|
||||
|
||||
// B is gone: a re-read surfaces BadNodeIdUnknown. The 1.5.378 node-cache read path
|
||||
@@ -263,7 +263,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
applier.MaterialiseHierarchy(withAB);
|
||||
applier.MaterialiseEquipmentTags(withAB);
|
||||
var nodeIdAString = V3NodeIds.Uns("eq-1", "A");
|
||||
sink.WriteValue(nodeIdAString, 10, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdAString, 10, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
using var session = await OpenSessionAsync($"opc.tcp://127.0.0.1:{port}/OtOpcUa", ct);
|
||||
var ns = (ushort)session.NamespaceUris.GetIndex(OtOpcUaNodeManager.DefaultNamespaceUri);
|
||||
@@ -296,7 +296,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
applier.AnnounceAddedNodes(plan);
|
||||
|
||||
// A stays alive.
|
||||
sink.WriteValue(nodeIdAString, 11, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdAString, 11, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
await WaitUntilAsync(() => { lock (gate) return receivedA.Any(v => Equals(v.Value, 11)); }, TimeSpan.FromSeconds(5));
|
||||
|
||||
// B is gone. (1.5.378 node-cache read throws for an unknown node — see above.)
|
||||
@@ -307,7 +307,7 @@ public sealed class SubscriptionSurvivalTests
|
||||
|
||||
// C is browsable + readable.
|
||||
var nodeIdC = new NodeId(V3NodeIds.Uns("eq-1", "C"), ns);
|
||||
sink.WriteValue(nodeIdC.Identifier.ToString()!, 9, Commons.OpcUa.OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue(nodeIdC.Identifier.ToString()!, 9, Commons.OpcUa.OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
var cRead = await session.ReadValueAsync(nodeIdC, ct);
|
||||
cRead.StatusCode.ShouldNotBe((StatusCode)StatusCodes.BadNodeIdUnknown);
|
||||
cRead.Value.ShouldBe(9);
|
||||
|
||||
+2
-2
@@ -156,9 +156,9 @@ public sealed class AddressSpaceApplierFailureSurfaceTests
|
||||
public bool ThrowOnAlarmWrite { get; init; }
|
||||
public bool ThrowOnMaterialiseAlarm { get; init; }
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
|
||||
+2
-2
@@ -323,12 +323,12 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality value.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Records an alarm condition write (stub implementation for testing).</summary>
|
||||
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Materialises an alarm condition (stub implementation for testing).</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
|
||||
|
||||
@@ -300,8 +300,8 @@ public sealed class AddressSpaceApplierRawUnsTests
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes")
|
||||
=> References.Add((sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType));
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void RebuildAddressSpace() { }
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) { }
|
||||
|
||||
@@ -147,7 +147,7 @@ public sealed class AddressSpaceApplierTests
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
var nodeId = V3NodeIds.Uns("eq-1", "Speed");
|
||||
// Terminal Bad written to the removed variable BEFORE the removal.
|
||||
sink.ValueWrites.ShouldContain((nodeId, OpcUaQuality.Bad));
|
||||
sink.ValueWrites.ShouldContain((nodeId, OpcUaStatus.Bad));
|
||||
sink.RemoveCalls.ShouldHaveSingleItem().ShouldBe(("var", nodeId));
|
||||
}
|
||||
|
||||
@@ -1583,22 +1583,22 @@ public sealed class AddressSpaceApplierTests
|
||||
public List<(string AlarmNodeId, string EquipmentNodeId, string DisplayName, string AlarmType, int Severity, bool IsNative)> AlarmConditionCalls => AlarmConditionQueue.ToList();
|
||||
|
||||
/// <summary>Gets the queue of value writes (NodeId, quality) — used to assert the PureRemove terminal Bad.</summary>
|
||||
public ConcurrentQueue<(string NodeId, OpcUaQuality Quality)> ValueWriteQueue { get; } = new();
|
||||
public ConcurrentQueue<(string NodeId, uint Quality)> ValueWriteQueue { get; } = new();
|
||||
/// <summary>Gets the list of recorded value writes.</summary>
|
||||
public List<(string NodeId, OpcUaQuality Quality)> ValueWrites => ValueWriteQueue.ToList();
|
||||
public List<(string NodeId, uint Quality)> ValueWrites => ValueWriteQueue.ToList();
|
||||
|
||||
/// <summary>Records a value write (NodeId + quality).</summary>
|
||||
/// <param name="nodeId">The node ID.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality.</param>
|
||||
/// <param name="statusCode">The OPC UA status code, sub-code included.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> ValueWriteQueue.Enqueue((nodeId, quality));
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> ValueWriteQueue.Enqueue((nodeId, statusCode));
|
||||
/// <summary>Records an alarm condition write call.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> AlarmQueue.Enqueue((alarmNodeId, state));
|
||||
/// <summary>Records an alarm-condition materialise call.</summary>
|
||||
@@ -1651,9 +1651,9 @@ public sealed class AddressSpaceApplierTests
|
||||
public int RebuildCalls;
|
||||
|
||||
/// <summary>Records a value write (no-op in this sink).</summary>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op alarm condition write call.</summary>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>No-op alarm-condition materialise call.</summary>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
||||
@@ -1679,15 +1679,15 @@ public sealed class AddressSpaceApplierTests
|
||||
/// <summary>Records a value write (no-op in this sink).</summary>
|
||||
/// <param name="nodeId">The node ID.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality.</param>
|
||||
/// <param name="statusCode">The OPC UA status code, sub-code included.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <summary>Throws an exception if configured to do so.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when configured to throw on alarm write.</exception>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
if (_throwOnAlarmWrite) throw new InvalidOperationException("simulated sink fault");
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
var deferred = new DeferredAddressSpaceSink();
|
||||
|
||||
// No throw, no observable side effect.
|
||||
deferred.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteValue("x", 1, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteAlarmCondition("a", Snapshot(active: true), DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.RebuildAddressSpace();
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
var inner = new RecordingSink();
|
||||
deferred.SetSink(inner);
|
||||
|
||||
deferred.WriteValue("x", 42, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteValue("x", 42, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteAlarmCondition("a-1", Snapshot(active: true), DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.RebuildAddressSpace();
|
||||
deferred.RaiseNodesAddedModelChange("eq-1", AddressSpaceRealm.Uns);
|
||||
@@ -42,11 +42,11 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
var deferred = new DeferredAddressSpaceSink();
|
||||
var inner = new RecordingSink();
|
||||
deferred.SetSink(inner);
|
||||
deferred.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteValue("x", 1, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
inner.Calls.Count.ShouldBe(1);
|
||||
|
||||
deferred.SetSink(null);
|
||||
deferred.WriteValue("y", 2, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns); // dropped
|
||||
deferred.WriteValue("y", 2, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns); // dropped
|
||||
inner.Calls.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
var second = new RecordingSink();
|
||||
|
||||
deferred.SetSink(first);
|
||||
deferred.WriteValue("a", 1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteValue("a", 1, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
deferred.SetSink(second);
|
||||
deferred.WriteValue("b", 2, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
deferred.WriteValue("b", 2, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
first.Calls.Single().ShouldBe("WV:a");
|
||||
second.Calls.Single().ShouldBe("WV:b");
|
||||
@@ -228,10 +228,10 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> CallQueue.Enqueue($"WV:{nodeId}");
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> CallQueue.Enqueue($"WA:{alarmNodeId}");
|
||||
/// <inheritdoc />
|
||||
@@ -288,9 +288,9 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { RemoveCalls.Add(("equipment", equipmentNodeId)); return Result; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
||||
|
||||
+5
-5
@@ -253,7 +253,7 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable
|
||||
var badSnapshot = new AlarmConditionSnapshot(
|
||||
Active: false, Acknowledged: true, Confirmed: true, Enabled: true,
|
||||
Shelving: AlarmShelvingKind.Unshelved, Severity: 700, Message: "HR200",
|
||||
Quality: OpcUaQuality.Bad);
|
||||
StatusCode: OpcUaStatus.Bad);
|
||||
nm.WriteAlarmCondition(RawAlarmPath, badSnapshot, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
|
||||
var condition = nm.TryGetAlarmCondition(RawAlarmPath, AddressSpaceRealm.Raw);
|
||||
@@ -261,7 +261,7 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable
|
||||
StatusCode.IsBad(condition.Quality.Value).ShouldBeTrue();
|
||||
|
||||
// Recover: a Good snapshot restores Good quality.
|
||||
var goodSnapshot = badSnapshot with { Quality = OpcUaQuality.Good };
|
||||
var goodSnapshot = badSnapshot with { StatusCode = OpcUaStatus.Good };
|
||||
nm.WriteAlarmCondition(RawAlarmPath, goodSnapshot, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
StatusCode.IsGood(condition.Quality.Value).ShouldBeTrue();
|
||||
}
|
||||
@@ -286,11 +286,11 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable
|
||||
var messageBefore = condition.Message!.Value?.Text;
|
||||
|
||||
// Device comes online → Good.
|
||||
nm.WriteAlarmQuality(RawAlarmPath, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
nm.WriteAlarmQuality(RawAlarmPath, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
StatusCode.IsGood(condition.Quality!.Value).ShouldBeTrue();
|
||||
|
||||
// Device goes comms-lost → Bad, but severity / message / active-ack state are untouched.
|
||||
nm.WriteAlarmQuality(RawAlarmPath, OpcUaQuality.Bad, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
nm.WriteAlarmQuality(RawAlarmPath, OpcUaStatus.Bad, DateTime.UtcNow, AddressSpaceRealm.Raw);
|
||||
StatusCode.IsBad(condition.Quality.Value).ShouldBeTrue();
|
||||
condition.Severity.Value.ShouldBe(severityBefore);
|
||||
condition.Message.Value?.Text.ShouldBe(messageBefore);
|
||||
@@ -307,7 +307,7 @@ public sealed class NodeManagerAlarmSourceFieldsTests : IDisposable
|
||||
var nm = host.Nm;
|
||||
|
||||
Should.NotThrow(() =>
|
||||
nm.WriteAlarmQuality("no/such/node", OpcUaQuality.Bad, DateTime.UtcNow, AddressSpaceRealm.Raw));
|
||||
nm.WriteAlarmQuality("no/such/node", OpcUaStatus.Bad, DateTime.UtcNow, AddressSpaceRealm.Raw));
|
||||
}
|
||||
|
||||
/// <summary>A booted server + its node manager, disposed via <c>await using</c> so an assertion failure
|
||||
|
||||
@@ -94,7 +94,7 @@ public sealed class NodeManagerArrayTests : IDisposable
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 3, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var payload = new[] { 1, 2, 3 };
|
||||
nm.WriteValue("eq-1/arrwrite", payload, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/arrwrite", payload, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/arrwrite");
|
||||
variable.ShouldNotBeNull();
|
||||
@@ -104,6 +104,47 @@ public sealed class NodeManagerArrayTests : IDisposable
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #519, second hop — the node manager hands the caller's status code to the SDK
|
||||
/// <b>verbatim</b>, sub-code included.
|
||||
/// <para>It used to re-expand a 3-state enum here (<c>Bad => StatusCodes.Bad</c>), so every Bad
|
||||
/// sub-code a driver produced landed on the wire as <c>0x80000000</c>. Boots a real SDK server and
|
||||
/// reads the created <see cref="BaseDataVariableState"/> back, which is the value an OPC UA client
|
||||
/// would receive.</para>
|
||||
/// <para>Asserting two DIFFERENT Bad sub-codes is what makes this falsifiable: under the old
|
||||
/// projection both collapsed to the same value, so a single-code assertion could pass on a
|
||||
/// hard-coded constant.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteValue_hands_the_status_sub_code_to_the_SDK_verbatim()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/subcode", parentFolderNodeId: null, displayName: "subcode", dataType: "Double",
|
||||
writable: false, historianTagname: null, realm: AddressSpaceRealm.Uns);
|
||||
|
||||
nm.WriteValue("eq-1/subcode", null, OpcUaStatus.BadNoCommunication, DateTime.UtcNow,
|
||||
realm: AddressSpaceRealm.Uns);
|
||||
|
||||
var variable = nm.TryGetVariable("eq-1/subcode");
|
||||
variable.ShouldNotBeNull();
|
||||
variable!.StatusCode.ShouldBe((StatusCode)StatusCodes.BadNoCommunication);
|
||||
variable.StatusCode.ShouldNotBe((StatusCode)StatusCodes.Bad,
|
||||
"the sub-code was collapsed to a generic Bad — this is exactly the #519 defect");
|
||||
|
||||
// A different Bad sub-code must land differently.
|
||||
nm.WriteValue("eq-1/subcode", null, 0x80740000u /* BadTypeMismatch */, DateTime.UtcNow,
|
||||
realm: AddressSpaceRealm.Uns);
|
||||
nm.TryGetVariable("eq-1/subcode")!.StatusCode.ShouldBe((StatusCode)StatusCodes.BadTypeMismatch);
|
||||
|
||||
// Uncertain and Good still round-trip.
|
||||
nm.WriteValue("eq-1/subcode", 1.0, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.TryGetVariable("eq-1/subcode")!.StatusCode.ShouldBe((StatusCode)StatusCodes.Good);
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync()
|
||||
{
|
||||
var host = new OpcUaApplicationHost(
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
|
||||
try
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
nm.WriteValue("eq-1/temp", 1.0, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns));
|
||||
nm.WriteValue("eq-1/temp", 1.0, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
+5
-5
@@ -45,7 +45,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 3.5f, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 3.5f, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.DataType.ShouldBe(DataTypeIds.Float); // arrange guard
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", (short)42, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", (short)42, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
node.ValueRank.ShouldBe(ValueRanks.Scalar); // arrange guard
|
||||
|
||||
@@ -97,7 +97,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 4u, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
node.ArrayDimensions![0].ShouldBe(4u); // arrange guard
|
||||
|
||||
@@ -122,7 +122,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
|
||||
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16",
|
||||
writable: false, historianTagname: null, isArray: true, arrayLength: 4u, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/buf")!;
|
||||
node.ValueRank.ShouldBe(ValueRanks.OneDimension); // arrange guard
|
||||
|
||||
@@ -172,7 +172,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7.0f, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7.0f, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
// Same DataType ("Float") + same scalar shape — only Writable flips false → true.
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
// Prior state = Good 7; the SDK then optimistically applied 42 (the write the device will reject).
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.Value = 42; // simulate the SDK's optimistic apply of the rejected write
|
||||
|
||||
@@ -79,7 +79,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.Value = 42;
|
||||
|
||||
@@ -102,7 +102,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
node.Value = 99; // a fresh poll already republished the confirmed register value (NOT the optimistic 42)
|
||||
|
||||
@@ -147,8 +147,8 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Raw", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Raw);
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Uns", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
var now = DateTime.UtcNow;
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Raw); // raw driver value
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Uns); // fanned UNS value
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaStatus.Good, now, AddressSpaceRealm.Raw); // raw driver value
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaStatus.Good, now, AddressSpaceRealm.Uns); // fanned UNS value
|
||||
var raw = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Raw)!;
|
||||
var uns = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Uns)!;
|
||||
|
||||
@@ -178,8 +178,8 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Raw", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Raw);
|
||||
nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Uns", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
var now = DateTime.UtcNow;
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Raw);
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaStatus.Good, now, AddressSpaceRealm.Raw);
|
||||
nm.WriteValue("A/B/C/D", 10, OpcUaStatus.Good, now, AddressSpaceRealm.Uns);
|
||||
var raw = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Raw)!;
|
||||
var uns = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Uns)!;
|
||||
|
||||
@@ -208,7 +208,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
var audit = nm.BuildWriteFailureAuditEvent(
|
||||
@@ -239,7 +239,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
nm.WriteValue("eq-1/sp", 7, OpcUaStatus.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
|
||||
var node = nm.TryGetVariable("eq-1/sp")!;
|
||||
|
||||
var audit = nm.BuildWriteFailureAuditEvent(
|
||||
|
||||
@@ -27,9 +27,9 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
|
||||
sink.WriteValue("eq-1/temp", 22.5, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/temp", 23.1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/pressure", 100, OpcUaQuality.Uncertain, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/temp", 22.5, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/temp", 23.1, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/pressure", 100, OpcUaStatus.Uncertain, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
server.NodeManager!.VariableCount.ShouldBe(2);
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
|
||||
sink.WriteAlarmCondition("alarm-7", Snapshot(active: true, acknowledged: false), DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/temp", 22.5, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("eq-1/temp", 22.5, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
|
||||
server.NodeManager!.VariableCount.ShouldBe(2);
|
||||
|
||||
@@ -120,8 +120,8 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
var (host, server) = await BootAsync();
|
||||
var sink = new SdkAddressSpaceSink(server.NodeManager!);
|
||||
|
||||
sink.WriteValue("a", 1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("b", 2, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("a", 1, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("b", 2, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteAlarmCondition("alarm-c", Snapshot(active: true), DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
server.NodeManager!.VariableCount.ShouldBe(3);
|
||||
|
||||
@@ -129,7 +129,7 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
server.NodeManager.VariableCount.ShouldBe(0);
|
||||
|
||||
// After rebuild, subsequent writes start fresh.
|
||||
sink.WriteValue("a", 99, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("a", 99, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
server.NodeManager.VariableCount.ShouldBe(1);
|
||||
|
||||
await host.DisposeAsync();
|
||||
@@ -142,7 +142,7 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable
|
||||
// Sanity check that the F10 fallback still works — production callers default to
|
||||
// NullOpcUaAddressSpaceSink when no SDK NodeManager is wired.
|
||||
var sink = NullOpcUaAddressSpaceSink.Instance;
|
||||
sink.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteValue("x", 1, OpcUaStatus.Good, DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.WriteAlarmCondition("a", Snapshot(active: true), DateTime.UtcNow, AddressSpaceRealm.Uns);
|
||||
sink.RebuildAddressSpace();
|
||||
await Task.CompletedTask;
|
||||
|
||||
+6
-6
@@ -63,7 +63,7 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
|
||||
driver.OnDataChange += (_, e) =>
|
||||
{
|
||||
published.Enqueue(e);
|
||||
var quality = e.Snapshot.StatusCode == 0u ? OpcUaQuality.Good : OpcUaQuality.Bad;
|
||||
var quality = e.Snapshot.StatusCode == 0u ? OpcUaStatus.Good : OpcUaStatus.Bad;
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"calc-drv", e.FullReference, e.Snapshot.Value, quality, Ts));
|
||||
};
|
||||
@@ -76,7 +76,7 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"src-drv", "src/a", 21.0, OpcUaQuality.Good, Ts));
|
||||
"src-drv", "src/a", 21.0, OpcUaStatus.Good, Ts));
|
||||
|
||||
// Leg 1 — dep flow: src/a=21 → A computes 42 and publishes on calc/engine/out.
|
||||
published.ShouldContain(e => e.FullReference == "calc/engine/out" && Equals(e.Snapshot.Value, 42.0));
|
||||
@@ -117,7 +117,7 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
|
||||
driver.OnDataChange += (_, e) =>
|
||||
{
|
||||
published.Enqueue(e);
|
||||
var quality = e.Snapshot.StatusCode == 0u ? OpcUaQuality.Good : OpcUaQuality.Bad;
|
||||
var quality = e.Snapshot.StatusCode == 0u ? OpcUaStatus.Good : OpcUaStatus.Bad;
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("calc-drv", e.FullReference, e.Snapshot.Value, quality, Ts));
|
||||
};
|
||||
var adapter = Sys.ActorOf(DependencyConsumerMuxAdapter.Props(driver, mux), "dep-adapter2");
|
||||
@@ -125,7 +125,7 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
|
||||
// Leg 1 — before redeploy: src/a=2 → keep=20, remove=7.
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("src-drv", "src/a", 2.0, OpcUaQuality.Good, Ts));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("src-drv", "src/a", 2.0, OpcUaStatus.Good, Ts));
|
||||
published.ShouldContain(e => e.FullReference == "calc/keep" && Equals(e.Snapshot.Value, 20.0));
|
||||
published.ShouldContain(e => e.FullReference == "calc/remove" && Equals(e.Snapshot.Value, 7.0));
|
||||
}, TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(100));
|
||||
@@ -149,8 +149,8 @@ public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
|
||||
// new result; the REMOVED tag never publishes a value derived from the post-redeploy input.
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("src-drv", "src/b", 50.0, OpcUaQuality.Good, Ts));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("src-drv", "src/a", 3.0, OpcUaQuality.Good, Ts));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("src-drv", "src/b", 50.0, OpcUaStatus.Good, Ts));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("src-drv", "src/a", 3.0, OpcUaStatus.Good, Ts));
|
||||
|
||||
// (a) new dep B flows through the RE-REGISTERED interest → new tag computes: 50 + 100 = 150.
|
||||
published.ShouldContain(e => e.FullReference == "calc/add" && Equals(e.Snapshot.Value, 150.0));
|
||||
|
||||
+5
-5
@@ -54,7 +54,7 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
|
||||
// The driver publishes by its wire-ref RawPath.
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 42.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 42.0, OpcUaStatus.Good, Ts));
|
||||
|
||||
var a = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
var b = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
@@ -68,7 +68,7 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
foreach (var u in byId.Values)
|
||||
{
|
||||
u.Value.ShouldBe(42.0);
|
||||
u.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
u.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
u.TimestampUtc.ShouldBe(Ts);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
var (actor, publish, _) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 5.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 5.0, OpcUaStatus.Good, Ts));
|
||||
|
||||
var only = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
only.NodeId.ShouldBe("Plant/Modbus/dev1/speed");
|
||||
@@ -114,7 +114,7 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
var (actor, publish, _) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 7.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 7.0, OpcUaStatus.Good, Ts));
|
||||
|
||||
var updates = new[]
|
||||
{
|
||||
@@ -144,7 +144,7 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
var (actor, publish, mux) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "Plant/Modbus/dev1/nope", 99.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/nope", 99.0, OpcUaStatus.Good, Ts));
|
||||
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
mux.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(5))
|
||||
|
||||
+4
-3
@@ -283,18 +283,19 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
|
||||
var (actor, publish) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
// Comms lost → the raw condition is annotated Bad.
|
||||
// Comms lost → the raw condition is annotated BadNoCommunication (#519: the reason survives to
|
||||
// the client; this used to be a bare Bad indistinguishable from any other fault).
|
||||
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: false));
|
||||
var bad = publish.ExpectMsg<OpcUaPublishActor.AlarmQualityUpdate>(TimeSpan.FromSeconds(5));
|
||||
bad.AlarmNodeId.ShouldBe(AlarmRawPath);
|
||||
bad.Realm.ShouldBe(AddressSpaceRealm.Raw);
|
||||
bad.Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
bad.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
|
||||
// Comms restored → annotated Good.
|
||||
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: true));
|
||||
var good = publish.ExpectMsg<OpcUaPublishActor.AlarmQualityUpdate>(TimeSpan.FromSeconds(5));
|
||||
good.AlarmNodeId.ShouldBe(AlarmRawPath);
|
||||
good.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
good.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
}
|
||||
|
||||
/// <summary>#477 Layer 2 — connectivity for a DIFFERENT driver instance annotates none of this driver's
|
||||
|
||||
@@ -122,18 +122,29 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
TimeSpan.FromSeconds(3));
|
||||
|
||||
// Driver fires an OnDataChange — actor should forward it to its parent as
|
||||
// AttributeValuePublished with Quality mapped from StatusCode.
|
||||
// AttributeValuePublished carrying the driver's status code.
|
||||
driver.FireDataChange("tag-a", value: 3.14, statusCode: 0u);
|
||||
|
||||
var published = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2));
|
||||
published.FullReference.ShouldBe("tag-a");
|
||||
published.Value.ShouldBe(3.14);
|
||||
published.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
published.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that subscribe translates OPC UA status severity bits to OpcUaQuality.</summary>
|
||||
/// <summary>
|
||||
/// Gitea #519 — a driver's status code reaches the parent <b>with its sub-code intact</b>.
|
||||
/// <para>This is the first of the two hops that used to destroy it: the actor projected the
|
||||
/// driver's <c>uint</c> onto a 3-state Good/Uncertain/Bad enum, and the node manager then
|
||||
/// re-expanded Bad to a bare <c>0x80000000</c>. The net effect was that no driver's sub-code ever
|
||||
/// reached an OPC UA client — <c>BadNoCommunication</c>, <c>BadTypeMismatch</c> and
|
||||
/// <c>BadNotSupported</c> were indistinguishable — while every driver maintained its own named
|
||||
/// constants (16 of which #497 had just corrected) as if they mattered end to end.</para>
|
||||
/// <para>The severity buckets are asserted too, since the bucket is still what
|
||||
/// <see cref="OpcUaStatus.IsGood"/> and friends classify on; what changed is that the bucket is no
|
||||
/// longer <em>all</em> that survives.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_translates_OPC_UA_status_severity_bits_to_OpcUaQuality()
|
||||
public async Task Subscribe_forwards_the_drivers_status_sub_code_without_collapsing_it()
|
||||
{
|
||||
var driver = new SubscribableStubDriver();
|
||||
var parent = CreateTestProbe();
|
||||
@@ -151,11 +162,25 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
|
||||
// Uncertain — severity bits 01 (top 2 bits = 01).
|
||||
driver.FireDataChange("tag-1", value: 1, statusCode: 0x40000000u);
|
||||
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().Quality.ShouldBe(OpcUaQuality.Uncertain);
|
||||
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(OpcUaStatus.Uncertain);
|
||||
|
||||
// Bad — severity bits 10.
|
||||
driver.FireDataChange("tag-1", value: 2, statusCode: 0x80000000u);
|
||||
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(OpcUaStatus.Bad);
|
||||
|
||||
// The regression itself: a Bad SUB-code must arrive intact, not flattened to OpcUaStatus.Bad.
|
||||
// This is the exact code MTConnect maps the Agent's UNAVAILABLE sentinel to, and the live gate
|
||||
// that opened #519 observed it reaching a client as 0x80000000.
|
||||
driver.FireDataChange("tag-1", value: null, statusCode: OpcUaStatus.BadNoCommunication);
|
||||
var noComms = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>();
|
||||
noComms.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
noComms.StatusCode.ShouldNotBe(OpcUaStatus.Bad, "the sub-code was collapsed to a generic Bad");
|
||||
OpcUaStatus.IsBad(noComms.StatusCode).ShouldBeTrue("it must still classify as Bad");
|
||||
|
||||
// A second, DIFFERENT Bad sub-code — the two were indistinguishable before the fix, so asserting
|
||||
// one alone would not prove the sub-code is carried rather than hard-coded.
|
||||
driver.FireDataChange("tag-1", value: null, statusCode: 0x80740000u /* BadTypeMismatch */);
|
||||
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(0x80740000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -163,7 +163,7 @@ public class NativeAlarmProjectorTests
|
||||
|
||||
var snap = sut.Project("n1", Evt(AlarmTransitionKind.Raise));
|
||||
|
||||
snap.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
snap.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
}
|
||||
|
||||
private static AlarmEventArgs Evt(
|
||||
|
||||
+3
-3
@@ -50,7 +50,7 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate(
|
||||
NodeId: "ns=2;s=tag-1",
|
||||
Value: 42,
|
||||
Quality: OpcUaQuality.Good,
|
||||
StatusCode: OpcUaStatus.Good,
|
||||
TimestampUtc: DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
|
||||
AwaitAssertion(() =>
|
||||
@@ -194,12 +194,12 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
|
||||
/// <param name="value">The value being written.</param>
|
||||
/// <param name="quality">The OPC UA quality status.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Writes++;
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Writes++;
|
||||
/// <summary>Records an alarm condition write.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="occurredUtc">The time the alarm occurred in UTC.</param>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Writes++;
|
||||
/// <summary>Materialises an alarm condition (stub implementation).</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier.</param>
|
||||
|
||||
+4
-4
@@ -123,8 +123,8 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
|
||||
/// <summary>A sink whose RebuildAddressSpace throws (drives the applier's SafeRebuild catch → RebuildFailed).</summary>
|
||||
private sealed class ThrowOnRebuildSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
@@ -138,8 +138,8 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
|
||||
/// <summary>A no-op sink — a clean apply (no failures).</summary>
|
||||
private sealed class NoopSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime occurredUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
||||
|
||||
@@ -622,13 +622,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="quality">The OPC UA quality code.</param>
|
||||
/// <param name="ts">The timestamp of the write.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"WV:{nodeId}");
|
||||
/// <summary>Records an alarm condition write call.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
/// <param name="state">The full condition state snapshot.</param>
|
||||
/// <param name="ts">The timestamp of the state change.</param>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
=> Calls.Enqueue($"WA:{alarmNodeId}");
|
||||
/// <summary>Records a materialise-alarm-condition call.</summary>
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
public void Accepts_message_contracts_without_pinned_dispatcher_in_tests()
|
||||
{
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests());
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=Tag1", 42.0, OpcUaQuality.Good, DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=Tag1", 42.0, OpcUaStatus.Good, DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
actor.Tell(new OpcUaPublishActor.AlarmStateUpdate("ns=2;s=Alarm1", Snapshot(active: true), DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
actor.Tell(new OpcUaPublishActor.ServiceLevelChanged(240));
|
||||
@@ -43,16 +43,16 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
var sink = new RecordingSink();
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink));
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=T1", 3.14, OpcUaQuality.Good, DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=T2", "abc", OpcUaQuality.Uncertain, DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=T1", 3.14, OpcUaStatus.Good, DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=T2", "abc", OpcUaStatus.Uncertain, DateTime.UtcNow, Realm: AddressSpaceRealm.Uns));
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
sink.Values.Count.ShouldBe(2);
|
||||
sink.Values[0].NodeId.ShouldBe("ns=2;s=T1");
|
||||
sink.Values[0].Value.ShouldBe(3.14);
|
||||
sink.Values[0].Quality.ShouldBe(OpcUaQuality.Good);
|
||||
sink.Values[1].Quality.ShouldBe(OpcUaQuality.Uncertain);
|
||||
sink.Values[0].StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
sink.Values[1].StatusCode.ShouldBe(OpcUaStatus.Uncertain);
|
||||
}, duration: PresenceBudget);
|
||||
}
|
||||
|
||||
@@ -84,14 +84,14 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink));
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.AlarmQualityUpdate(
|
||||
"Plant/Modbus/dev1/temp_hi", OpcUaQuality.Bad, DateTime.UtcNow, Realm: AddressSpaceRealm.Raw));
|
||||
"Plant/Modbus/dev1/temp_hi", OpcUaStatus.Bad, DateTime.UtcNow, Realm: AddressSpaceRealm.Raw));
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
sink.AlarmQualityQueue.Count.ShouldBe(1);
|
||||
sink.AlarmQualityQueue.TryPeek(out var q).ShouldBeTrue();
|
||||
q.AlarmNodeId.ShouldBe("Plant/Modbus/dev1/temp_hi");
|
||||
q.Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
q.StatusCode.ShouldBe(OpcUaStatus.Bad);
|
||||
q.Realm.ShouldBe(AddressSpaceRealm.Raw);
|
||||
}, duration: PresenceBudget);
|
||||
}
|
||||
@@ -702,11 +702,11 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
private sealed class RecordingSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
/// <summary>Gets the queue of recorded value updates.</summary>
|
||||
public ConcurrentQueue<(string NodeId, object? Value, OpcUaQuality Quality, DateTime Ts)> ValueQueue { get; } = new();
|
||||
public ConcurrentQueue<(string NodeId, object? Value, uint StatusCode, DateTime Ts)> ValueQueue { get; } = new();
|
||||
/// <summary>Gets the queue of recorded alarm condition updates.</summary>
|
||||
public ConcurrentQueue<(string AlarmNodeId, AlarmConditionSnapshot State, DateTime Ts)> AlarmQueue { get; } = new();
|
||||
/// <summary>Gets the queue of recorded alarm-quality annotations (#477).</summary>
|
||||
public ConcurrentQueue<(string AlarmNodeId, OpcUaQuality Quality, DateTime Ts, AddressSpaceRealm Realm)> AlarmQualityQueue { get; } = new();
|
||||
public ConcurrentQueue<(string AlarmNodeId, uint StatusCode, DateTime Ts, AddressSpaceRealm Realm)> AlarmQualityQueue { get; } = new();
|
||||
/// <summary>Count of rebuild calls.</summary>
|
||||
public int RebuildCalls;
|
||||
/// <summary>Gets the queue of recorded EnsureFolder calls.</summary>
|
||||
@@ -717,7 +717,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
public ConcurrentQueue<string> ModelChangeQueue { get; } = new();
|
||||
|
||||
/// <summary>Gets the list of recorded value updates.</summary>
|
||||
public List<(string NodeId, object? Value, OpcUaQuality Quality, DateTime Ts)> Values =>
|
||||
public List<(string NodeId, object? Value, uint StatusCode, DateTime Ts)> Values =>
|
||||
ValueQueue.ToList();
|
||||
/// <summary>Gets the list of recorded alarm condition updates.</summary>
|
||||
public List<(string AlarmNodeId, AlarmConditionSnapshot State, DateTime Ts)> Alarms =>
|
||||
@@ -734,10 +734,10 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
/// <summary>Records a value update.</summary>
|
||||
/// <param name="nodeId">The OPC UA node identifier.</param>
|
||||
/// <param name="value">The attribute value.</param>
|
||||
/// <param name="quality">The OPC UA quality code.</param>
|
||||
/// <param name="statusCode">The OPC UA status code, sub-code included.</param>
|
||||
/// <param name="ts">The timestamp of the update.</param>
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
ValueQueue.Enqueue((nodeId, value, quality, ts));
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
ValueQueue.Enqueue((nodeId, value, statusCode, ts));
|
||||
|
||||
/// <summary>Records an alarm condition update.</summary>
|
||||
/// <param name="alarmNodeId">The OPC UA alarm node identifier.</param>
|
||||
@@ -747,8 +747,8 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
AlarmQueue.Enqueue((alarmNodeId, state, ts));
|
||||
|
||||
/// <summary>Records an alarm-quality annotation (#477).</summary>
|
||||
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
AlarmQualityQueue.Enqueue((alarmNodeId, quality, ts, realm));
|
||||
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime ts, AddressSpaceRealm realm = AddressSpaceRealm.Uns) =>
|
||||
AlarmQualityQueue.Enqueue((alarmNodeId, statusCode, ts, realm));
|
||||
|
||||
/// <summary>Materialises an alarm condition (no-op in test).</summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID.</param>
|
||||
|
||||
+4
-4
@@ -700,9 +700,9 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
// Baseline is Good (unread inputs are not a quality signal — no load-time annotation). Drive the
|
||||
// input Bad with a value below threshold: the predicate freezes (no transition), but the worst-input
|
||||
// quality bucket moves Good→Bad → a QualityChanged annotation flows out of band.
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 10, DateTime.UtcNow, OpcUaQuality.Bad));
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 10, DateTime.UtcNow, OpcUaStatus.Bad));
|
||||
|
||||
var q = publish.FishForMessage<OpcUaPublishActor.AlarmQualityUpdate>(m => m.Quality == OpcUaQuality.Bad, Timeout);
|
||||
var q = publish.FishForMessage<OpcUaPublishActor.AlarmQualityUpdate>(m => m.StatusCode == OpcUaStatus.Bad, Timeout);
|
||||
q.AlarmNodeId.ShouldBe("alm-1");
|
||||
q.Realm.ShouldBe(AddressSpaceRealm.Uns);
|
||||
|
||||
@@ -726,11 +726,11 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
|
||||
|
||||
// Above threshold (activates) but Uncertain quality — Uncertain is "ready", so the predicate runs.
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow, OpcUaQuality.Uncertain));
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow, OpcUaStatus.Uncertain));
|
||||
|
||||
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
|
||||
state.AlarmNodeId.ShouldBe("alm-1");
|
||||
state.State.Quality.ShouldBe(OpcUaQuality.Uncertain);
|
||||
state.State.StatusCode.ShouldBe(OpcUaStatus.Uncertain);
|
||||
}
|
||||
|
||||
/// <summary>OneShotShelve transition carries the operator's identity: an operator-driven OneShotShelve
|
||||
|
||||
+14
-14
@@ -21,9 +21,9 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1", "tag-2" }, subA.Ref));
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-2", "tag-3" }, subB.Ref));
|
||||
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-3", 30, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-3", 30, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
|
||||
// subA hears tag-1 + tag-2.
|
||||
var aMsgs = new[]
|
||||
@@ -53,7 +53,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
var sub = CreateTestProbe();
|
||||
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "nobody-cares", 99, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "nobody-cares", 99, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
|
||||
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
@@ -66,11 +66,11 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
var sub = CreateTestProbe();
|
||||
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>();
|
||||
|
||||
mux.Tell(new DependencyMuxActor.UnregisterInterest(sub.Ref));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 20, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 20, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
@@ -84,10 +84,10 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-2" }, sub.Ref)); // replaces tag-1
|
||||
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-1", 10, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "tag-2", 20, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("tag-2");
|
||||
}
|
||||
|
||||
@@ -110,17 +110,17 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"driver-1", "ref-a", 10, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
"driver-1", "ref-a", 10, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>(TimeSpan.FromMilliseconds(200))
|
||||
.Value.ShouldBe(10);
|
||||
}, duration: TimeSpan.FromSeconds(3));
|
||||
|
||||
// From here the actor is wired — second publish drives the sum.
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "ref-b", 32, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "ref-b", 32, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>(TimeSpan.FromSeconds(2)).Value.ShouldBe(42);
|
||||
|
||||
// Unrelated ref shouldn't fire eval.
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "ref-unrelated", 99, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished("driver-1", "ref-unrelated", 99, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
|
||||
// Tell the host an AttributeValuePublished — it should fan out to the mux + subscriber.
|
||||
hostActor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"driver-1", "ref-1", 42, OpcUaQuality.Good, DateTime.UtcNow));
|
||||
"driver-1", "ref-1", 42, OpcUaStatus.Good, DateTime.UtcNow));
|
||||
|
||||
subscriber.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("ref-1");
|
||||
}
|
||||
@@ -159,9 +159,9 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
|
||||
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"driver-1", "tag-1", 10, OpcUaQuality.Bad, DateTime.UtcNow));
|
||||
"driver-1", "tag-1", 10, OpcUaStatus.Bad, DateTime.UtcNow));
|
||||
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().StatusCode.ShouldBe(OpcUaStatus.Bad);
|
||||
}
|
||||
|
||||
private sealed class EchoSumEvaluator : ZB.MOM.WW.OtOpcUa.Commons.Engines.IVirtualTagEvaluator
|
||||
|
||||
@@ -84,7 +84,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
// 02/S13: the failure now ALSO degrades the node — with no declared dependencyRefs the
|
||||
// inputs-ready gate is vacuously satisfied, so a Bad EvaluationResult reaches the parent (this
|
||||
// replaces the old ExpectNoMsg, which encoded the stale-Good bug).
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
}
|
||||
|
||||
/// <summary>02/S13 once-per-transition: two consecutive failures publish exactly ONE Bad
|
||||
@@ -98,7 +98,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "expr", evaluator: evaluator));
|
||||
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
|
||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
@@ -116,15 +116,15 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||
var first = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
|
||||
first.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
first.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
first.Value.ShouldBe(42);
|
||||
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 3, DateTime.UtcNow));
|
||||
var recovered = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
|
||||
recovered.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
recovered.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
recovered.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
}
|
||||
|
||||
/// <summary>02/S13 inputs-ready gate: a multi-dep script that fails before all its declared
|
||||
@@ -177,7 +177,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
|
||||
// "b" arrives — all refs now seen, so the failure degrades to Bad.
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("b", 2, DateTime.UtcNow));
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
}
|
||||
|
||||
/// <summary>Test evaluator returning a fixed sequence of results, one per Evaluate call; the last
|
||||
|
||||
+17
-12
@@ -67,7 +67,7 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
update.NodeId.ShouldBe("eq-1/speed-rpm");
|
||||
update.Value.ShouldBe(42.0);
|
||||
update.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
update.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
update.TimestampUtc.ShouldBe(ts);
|
||||
}
|
||||
|
||||
@@ -433,19 +433,21 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
// First dep change → evaluator Ok(42) → Good publish.
|
||||
child.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, ts1));
|
||||
var good = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
good.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
good.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
good.Value.ShouldBe(42.0);
|
||||
|
||||
// Second dep change → evaluator Failure → Bad publish carrying the last-known value 42.
|
||||
// #519: the child names the reason (BadInternalError) instead of a bare Bad, and the host bridges
|
||||
// it verbatim — so this asserts the sub-code a client would actually read.
|
||||
child.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, ts2));
|
||||
var bad = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
bad.Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
bad.StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
bad.Value.ShouldBe(42.0);
|
||||
bad.TimestampUtc.ShouldBe(ts2);
|
||||
}
|
||||
|
||||
/// <summary>02/S13 bridge: a Bad-quality <see cref="VirtualTagActor.EvaluationResult"/> must bridge to
|
||||
/// an <see cref="OpcUaPublishActor.AttributeValueUpdate"/> carrying <see cref="OpcUaQuality.Bad"/>
|
||||
/// an <see cref="OpcUaPublishActor.AttributeValueUpdate"/> carrying <see cref="OpcUaStatus.Bad"/>
|
||||
/// (not the old hard-coded Good).</summary>
|
||||
[Fact]
|
||||
public void Bad_quality_EvaluationResult_is_bridged_as_Bad()
|
||||
@@ -457,15 +459,18 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
|
||||
var ts = new DateTime(2026, 7, 12, 12, 0, 0, DateTimeKind.Utc);
|
||||
host.Tell(new VirtualTagActor.EvaluationResult(
|
||||
"vt-1", 42.0, ts, CorrelationId.NewId(), OpcUaQuality.Bad));
|
||||
"vt-1", 42.0, ts, CorrelationId.NewId(), OpcUaStatus.Bad));
|
||||
|
||||
var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
update.Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
update.StatusCode.ShouldBe(OpcUaStatus.Bad);
|
||||
update.Value.ShouldBe(42.0);
|
||||
}
|
||||
|
||||
/// <summary>02/S13 historize parity: a Bad result on a historized plan must record the dormant
|
||||
/// engine's BadInternalError status (0x80020000u) in the snapshot, not the Good 0u.</summary>
|
||||
/// <summary>02/S13 historize parity: a failed result on a historized plan must record BadInternalError
|
||||
/// (0x80020000u) in the snapshot, not the Good 0u.
|
||||
/// <para>#519 — the host no longer DERIVES this from a Bad bucket; it records the result's status code
|
||||
/// verbatim, and <see cref="VirtualTagActor"/> is what stamps BadInternalError. So this constructs the
|
||||
/// result the real child emits, and the assertion now proves pass-through rather than re-derivation.</para></summary>
|
||||
[Fact]
|
||||
public void Historized_Bad_result_is_recorded_with_BadInternalError_status()
|
||||
{
|
||||
@@ -478,12 +483,12 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
|
||||
var ts = new DateTime(2026, 7, 12, 12, 0, 0, DateTimeKind.Utc);
|
||||
host.Tell(new VirtualTagActor.EvaluationResult(
|
||||
"vt-1", 42.0, ts, CorrelationId.NewId(), OpcUaQuality.Bad));
|
||||
"vt-1", 42.0, ts, CorrelationId.NewId(), OpcUaStatus.BadInternalError));
|
||||
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
AwaitAssert(() => writer.Calls.Count.ShouldBe(1));
|
||||
writer.Calls.TryPeek(out var captured).ShouldBeTrue();
|
||||
captured.Value.StatusCode.ShouldBe(0x80020000u); // BadInternalError — dormant-engine parity
|
||||
captured.Value.StatusCode.ShouldBe(OpcUaStatus.BadInternalError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -514,7 +519,7 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
child.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||
var first = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
first.Value.ShouldBe(7.0);
|
||||
first.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
first.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
|
||||
// The redeploy re-materialises the node to BadWaitingForInitialData and re-applies the UNCHANGED
|
||||
// plan — the child survives with its dedup state. The fix re-asserts the last value; without it the
|
||||
@@ -523,7 +528,7 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
var reasserted = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
reasserted.NodeId.ShouldBe("eq-1/speed-rpm");
|
||||
reasserted.Value.ShouldBe(7.0);
|
||||
reasserted.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
reasserted.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
|
||||
// No child churn on the unchanged redeploy — the re-assert must not respawn or re-register.
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
|
||||
Reference in New Issue
Block a user