test(alarms): assert reconnect-dropped native alarm does not dead-letter; tighten severity doc
v2-ci / build (push) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Add AllDeadLetters probe to Native_alarm_during_reconnect_is_dropped_not_forwarded so the test genuinely guards the Reconnecting state's Receive<NativeAlarmRaised> drop handler — removing that handler would now cause a dead-letter and fail the assertion (false-negative gap closed). Reword the ScriptedAlarms.md severity-mapping note: "snaps on the first transition" → "every transition maps … overriding the authored seed from the first transition onward", clarifying that MapSeverity runs on every event, not just the first.
This commit is contained in:
@@ -145,8 +145,9 @@ runtime.
|
|||||||
#### Severity mapping
|
#### Severity mapping
|
||||||
|
|
||||||
The authored `severity` (1–1000) seeds the OPC UA condition node at materialisation
|
The authored `severity` (1–1000) seeds the OPC UA condition node at materialisation
|
||||||
time. On the **first native transition** the value snaps to one of four fixed buckets
|
time. Every native transition maps the driver's `AlarmSeverity` to one of four fixed
|
||||||
via `NativeAlarmProjector.MapSeverity`:
|
buckets via `NativeAlarmProjector.MapSeverity`, overriding the authored `severity` seed
|
||||||
|
from the first transition onward:
|
||||||
|
|
||||||
| `AlarmSeverity` enum | Projected value |
|
| `AlarmSeverity` enum | Projected value |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -168,8 +169,8 @@ by `OtOpcUaNodeManager.MapSeverity` on each `AlarmStateUpdate` write:
|
|||||||
| ≥ 800 | `High` |
|
| ≥ 800 | `High` |
|
||||||
|
|
||||||
Consequently the four authored buckets land as: Low→`MediumLow`, Medium→`Medium`,
|
Consequently the four authored buckets land as: Low→`MediumLow`, Medium→`Medium`,
|
||||||
High→`MediumHigh`, Critical→`High`. The authored `severity` field has no effect after
|
High→`MediumHigh`, Critical→`High`. The authored `severity` field is overridden by
|
||||||
the first transition.
|
live driver events on every transition.
|
||||||
|
|
||||||
### Runtime flow
|
### Runtime flow
|
||||||
|
|
||||||
|
|||||||
+29
-4
@@ -1,4 +1,5 @@
|
|||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
|
using Akka.Event;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||||
@@ -94,8 +95,10 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A native alarm transition that races in while the actor is in <c>Reconnecting</c> is silently
|
/// A native alarm transition that races in while the actor is in <c>Reconnecting</c> is silently
|
||||||
/// dropped (debug log only) — it NEVER reaches the parent as
|
/// dropped (debug log only) — it NEVER reaches the parent as
|
||||||
/// <see cref="DriverInstanceActor.AttributeAlarmPublished"/> and does NOT dead-letter. The feed
|
/// <see cref="DriverInstanceActor.AttributeAlarmPublished"/> AND is NOT dead-lettered (the
|
||||||
/// re-delivers active alarms once the actor re-enters <c>Connected</c>, so dropping here is safe.
|
/// <c>Reconnecting</c> state's explicit <c>Receive<NativeAlarmRaised></c> handler consumes and
|
||||||
|
/// discards it, so it never becomes unhandled). The feed re-delivers active alarms once the actor
|
||||||
|
/// re-enters <c>Connected</c>, so dropping here is safe.
|
||||||
/// (<see cref="DriverInstanceActor"/> line ~345: the <c>Reconnecting</c> state's
|
/// (<see cref="DriverInstanceActor"/> line ~345: the <c>Reconnecting</c> state's
|
||||||
/// <c>Receive<NativeAlarmRaised></c> logs a debug message and discards.)
|
/// <c>Receive<NativeAlarmRaised></c> logs a debug message and discards.)
|
||||||
///
|
///
|
||||||
@@ -107,10 +110,27 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
|
|||||||
/// (detaches handler), then processes the queued <c>NativeAlarmRaised</c> in Reconnecting
|
/// (detaches handler), then processes the queued <c>NativeAlarmRaised</c> in Reconnecting
|
||||||
/// → drops it. The default 10 s reconnect interval ensures no retry fires during the check.
|
/// → drops it. The default 10 s reconnect interval ensures no retry fires during the check.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Both properties are actively asserted: (a) the parent probe receives no
|
||||||
|
/// <see cref="DriverInstanceActor.AttributeAlarmPublished"/>; (b) a dead-letter probe subscribed
|
||||||
|
/// to <see cref="AllDeadLetters"/> on the <see cref="ActorSystem.EventStream"/> also receives
|
||||||
|
/// nothing — proving the drop handler is present (removing it would cause a dead-letter and fail
|
||||||
|
/// this assertion). <c>NativeAlarmRaised</c> is private to the actor, so the dead-letter probe
|
||||||
|
/// subscribes to the unfiltered <see cref="AllDeadLetters"/> channel; this is safe because
|
||||||
|
/// exactly one message is injected and the reconnect timer (10 s) cannot fire in the window.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Native_alarm_during_reconnect_is_dropped_not_forwarded()
|
public void Native_alarm_during_reconnect_is_dropped_not_forwarded()
|
||||||
{
|
{
|
||||||
|
// Subscribe a dead-letter probe BEFORE injecting the alarm so we don't miss any early publish.
|
||||||
|
// NativeAlarmRaised is private, so we subscribe to the unfiltered AllDeadLetters channel.
|
||||||
|
// Only one message is injected and the 10 s reconnect timer can't fire in this window, so
|
||||||
|
// a plain "no dead letters at all" assertion is safe and non-flaky.
|
||||||
|
var deadLetters = CreateTestProbe();
|
||||||
|
Sys.EventStream.Subscribe(deadLetters.Ref, typeof(AllDeadLetters));
|
||||||
|
|
||||||
// Long reconnect interval (default 10 s) so the retry doesn't fire during the assertion window.
|
// Long reconnect interval (default 10 s) so the retry doesn't fire during the assertion window.
|
||||||
var driver = new AlarmSourceStubDriver();
|
var driver = new AlarmSourceStubDriver();
|
||||||
var parent = CreateTestProbe();
|
var parent = CreateTestProbe();
|
||||||
@@ -134,13 +154,18 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
|
|||||||
SourceTimestampUtc: DateTime.UtcNow,
|
SourceTimestampUtc: DateTime.UtcNow,
|
||||||
Kind: AlarmTransitionKind.Raise));
|
Kind: AlarmTransitionKind.Raise));
|
||||||
|
|
||||||
|
// (a) The parent must NOT receive AttributeAlarmPublished from that alarm.
|
||||||
// The actor processes: (1) ForceReconnect → Reconnecting (handler detached);
|
// The actor processes: (1) ForceReconnect → Reconnecting (handler detached);
|
||||||
// (2) NativeAlarmRaised → dropped (debug log, no forward).
|
// (2) NativeAlarmRaised → dropped (debug log, no forward).
|
||||||
// The parent must NOT receive AttributeAlarmPublished from that alarm.
|
|
||||||
// Wait generously — the default reconnect interval of 10 s means no retry fires here.
|
// Wait generously — the default reconnect interval of 10 s means no retry fires here.
|
||||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||||
|
|
||||||
// The actor must still be alive — Watch + no Terminated (not crashed or dead-lettered).
|
// (b) The alarm must also NOT have dead-lettered — the Reconnecting state's explicit
|
||||||
|
// Receive<NativeAlarmRaised> handler must have consumed it. If that handler were removed the
|
||||||
|
// message would become unhandled → AllDeadLetters, and this assertion would catch the regression.
|
||||||
|
deadLetters.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||||
|
|
||||||
|
// The actor must still be alive — Watch + no Terminated.
|
||||||
Watch(actor);
|
Watch(actor);
|
||||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user