feat(siteruntime): NativeAlarmActor mirrors source alarms (snapshot swap, retention, persistence)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Task 15: NativeAlarmActor mirrors a source's native alarms — subscribe on start,
|
||||
/// emit enriched AlarmStateChanged, snapshot atomic swap, retention, persistence.
|
||||
/// </summary>
|
||||
public class NativeAlarmActorTests : TestKit, IDisposable
|
||||
{
|
||||
private readonly string _dbFile;
|
||||
private readonly SiteStorageService _storage;
|
||||
private readonly SiteRuntimeOptions _options = new();
|
||||
|
||||
public NativeAlarmActorTests()
|
||||
{
|
||||
_dbFile = Path.Combine(Path.GetTempPath(), $"naa-{Guid.NewGuid():N}.db");
|
||||
_storage = new SiteStorageService($"Data Source={_dbFile}", NullLogger<SiteStorageService>.Instance);
|
||||
_storage.InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static ResolvedNativeAlarmSource Source() => new()
|
||||
{
|
||||
CanonicalName = "Pressure",
|
||||
ConnectionName = "Opc",
|
||||
SourceReference = "ns=2;s=T01"
|
||||
};
|
||||
|
||||
private static NativeAlarmTransition Transition(
|
||||
string sourceRef, AlarmTransitionKind kind, AlarmConditionState condition, DateTimeOffset? time = null) =>
|
||||
new(sourceRef, "T01", "AnalogLimit.Hi", kind, condition,
|
||||
"Process", "hi", "hi", "", "", null, time ?? DateTimeOffset.UtcNow, "92", "90");
|
||||
|
||||
private IActorRef Spawn(IActorRef instanceActor, IActorRef dclManager) =>
|
||||
ActorOf(Props.Create(() => new NativeAlarmActor(
|
||||
Source(), "inst", instanceActor, dclManager, _storage, _options, NullLogger<NativeAlarmActor>.Instance)));
|
||||
|
||||
[Fact]
|
||||
public void SubscribeOnStart_SendsRequestForSourceBinding()
|
||||
{
|
||||
var dcl = CreateTestProbe();
|
||||
Spawn(CreateTestProbe().Ref, dcl.Ref);
|
||||
|
||||
var req = dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
Assert.Equal("inst", req.InstanceUniqueName);
|
||||
Assert.Equal("Opc", req.ConnectionName);
|
||||
Assert.Equal("ns=2;s=T01", req.SourceReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Raise_EmitsEnrichedAlarmStateChanged()
|
||||
{
|
||||
var instance = CreateTestProbe();
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = Spawn(instance.Ref, dcl.Ref);
|
||||
dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.Raise,
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
|
||||
|
||||
var emitted = instance.ExpectMsg<AlarmStateChanged>();
|
||||
Assert.Equal(AlarmKind.NativeOpcUa, emitted.Kind);
|
||||
Assert.Equal("T01.Hi", emitted.SourceReference);
|
||||
Assert.Equal(AlarmState.Active, emitted.State);
|
||||
Assert.Equal(800, emitted.Condition.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal()
|
||||
{
|
||||
var instance = CreateTestProbe();
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = Spawn(instance.Ref, dcl.Ref);
|
||||
dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.Raise,
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
|
||||
instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);
|
||||
|
||||
// A fresh snapshot that no longer contains T01.Hi means it cleared at the source.
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.SnapshotComplete,
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
|
||||
|
||||
var cleared = instance.ExpectMsg<AlarmStateChanged>();
|
||||
Assert.Equal("T01.Hi", cleared.SourceReference);
|
||||
Assert.Equal(AlarmState.Normal, cleared.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OlderTransition_IsIgnored()
|
||||
{
|
||||
var instance = CreateTestProbe();
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = Spawn(instance.Ref, dcl.Ref);
|
||||
dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
|
||||
var t0 = DateTimeOffset.UtcNow;
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.Raise,
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800), t0)));
|
||||
instance.ExpectMsg<AlarmStateChanged>(m => m.State == AlarmState.Active);
|
||||
|
||||
// An out-of-order (older) transition must not overwrite newer state.
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.Clear,
|
||||
new AlarmConditionState(false, true, null, AlarmShelveState.Unshelved, false, 0), t0.AddSeconds(-30))));
|
||||
|
||||
instance.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Shutdown();
|
||||
if (File.Exists(_dbFile))
|
||||
{
|
||||
File.Delete(_dbFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user