5d07ac24cb
InstanceActor.BuildAlarmStatesSnapshot now adds an IsConfiguredPlaceholder row per configured native source binding that currently has no live condition, so the Debug View tree can show the binding node even when quiet. A binding is "quiet" when no retained AlarmStateChanged carries its NativeSourceCanonicalName (DV-1). Kind derivation: reuses the exact nativeKind value already computed via ResolveNativeKind(nativeSource.ConnectionName) at the NativeAlarmActor creation site and stored in a new _nativeAlarmKinds dictionary -- the accurate per-binding kind (NativeOpcUa vs NativeMxAccess), not the NativeOpcUa default. Tests: Snapshot_QuietNativeBinding_EmitsPlaceholder, Snapshot_NativeBindingWithLiveCondition_NoPlaceholder.
154 lines
6.2 KiB
C#
154 lines
6.2 KiB
C#
using System.Text.Json;
|
|
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.DebugView;
|
|
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;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
|
|
|
/// <summary>
|
|
/// Task 16: InstanceActor spawns NativeAlarmActor children from the flattened
|
|
/// configuration and surfaces enriched native alarm state in the DebugView snapshot.
|
|
/// </summary>
|
|
public class InstanceActorNativeAlarmTests : TestKit, IDisposable
|
|
{
|
|
private readonly SiteStorageService _storage;
|
|
private readonly ScriptCompilationService _compilationService;
|
|
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
|
private readonly SiteRuntimeOptions _options = new();
|
|
private readonly string _dbFile;
|
|
|
|
public InstanceActorNativeAlarmTests()
|
|
{
|
|
_dbFile = Path.Combine(Path.GetTempPath(), $"instance-native-{Guid.NewGuid():N}.db");
|
|
_storage = new SiteStorageService($"Data Source={_dbFile}", NullLogger<SiteStorageService>.Instance);
|
|
_storage.InitializeAsync().GetAwaiter().GetResult();
|
|
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
|
_sharedScriptLibrary = new SharedScriptLibrary(_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
|
}
|
|
|
|
private IActorRef CreateInstanceActorWithDcl(string instanceName, FlattenedConfiguration config, IActorRef dclManager) =>
|
|
ActorOf(Props.Create(() => new InstanceActor(
|
|
instanceName,
|
|
JsonSerializer.Serialize(config),
|
|
_storage,
|
|
_compilationService,
|
|
_sharedScriptLibrary,
|
|
null,
|
|
_options,
|
|
NullLogger<InstanceActor>.Instance,
|
|
dclManager)));
|
|
|
|
private static FlattenedConfiguration ConfigWithNativeSource(string instanceName) => new()
|
|
{
|
|
InstanceUniqueName = instanceName,
|
|
NativeAlarmSources =
|
|
[
|
|
new ResolvedNativeAlarmSource
|
|
{
|
|
CanonicalName = "Pressure",
|
|
ConnectionName = "Opc",
|
|
SourceReference = "ns=2;s=T01"
|
|
}
|
|
]
|
|
};
|
|
|
|
[Fact]
|
|
public void SpawnsNativeAlarmActor_WhichSubscribesViaDcl()
|
|
{
|
|
var dcl = CreateTestProbe();
|
|
CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), 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 DebugViewSnapshot_IncludesNativeAlarm_AfterTransition()
|
|
{
|
|
var dcl = CreateTestProbe();
|
|
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
|
|
|
|
// Simulate the NativeAlarmActor emitting an enriched event upward.
|
|
actor.Tell(new AlarmStateChanged("inst", "T01.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
|
|
{
|
|
Kind = AlarmKind.NativeOpcUa,
|
|
SourceReference = "T01.Hi",
|
|
Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)
|
|
});
|
|
|
|
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
|
|
var snap = ExpectMsg<DebugViewSnapshot>();
|
|
|
|
Assert.Contains(snap.AlarmStates, a =>
|
|
a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
|
|
}
|
|
|
|
[Fact]
|
|
public void Snapshot_QuietNativeBinding_EmitsPlaceholder()
|
|
{
|
|
var dcl = CreateTestProbe();
|
|
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
|
|
|
|
// No live condition is emitted: the configured "Pressure" binding is quiet.
|
|
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
|
|
var snap = ExpectMsg<DebugViewSnapshot>();
|
|
|
|
var placeholders = snap.AlarmStates
|
|
.Where(a => a.NativeSourceCanonicalName == "Pressure" && a.IsConfiguredPlaceholder)
|
|
.ToList();
|
|
|
|
Assert.Single(placeholders);
|
|
var placeholder = placeholders[0];
|
|
Assert.Equal(AlarmState.Normal, placeholder.State);
|
|
Assert.Equal("Pressure", placeholder.NativeSourceCanonicalName);
|
|
Assert.Equal(AlarmKind.NativeOpcUa, placeholder.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void Snapshot_NativeBindingWithLiveCondition_NoPlaceholder()
|
|
{
|
|
var dcl = CreateTestProbe();
|
|
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
|
|
|
|
// The NativeAlarmActor emits a live condition stamped with the binding's
|
|
// canonical name (DV-1: NativeSourceCanonicalName), so the binding is "active".
|
|
actor.Tell(new AlarmStateChanged("inst", "Pressure.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
|
|
{
|
|
Kind = AlarmKind.NativeOpcUa,
|
|
SourceReference = "ns=2;s=T01.Hi",
|
|
NativeSourceCanonicalName = "Pressure",
|
|
Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)
|
|
});
|
|
|
|
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
|
|
var snap = ExpectMsg<DebugViewSnapshot>();
|
|
|
|
// The live condition is present...
|
|
Assert.Contains(snap.AlarmStates, a =>
|
|
a.SourceReference == "ns=2;s=T01.Hi" && a.NativeSourceCanonicalName == "Pressure"
|
|
&& a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
|
|
|
|
// ...and NO placeholder row is emitted for that binding.
|
|
Assert.DoesNotContain(snap.AlarmStates, a =>
|
|
a.NativeSourceCanonicalName == "Pressure" && a.IsConfiguredPlaceholder);
|
|
}
|
|
|
|
void IDisposable.Dispose()
|
|
{
|
|
Shutdown();
|
|
try { File.Delete(_dbFile); } catch { /* cleanup */ }
|
|
}
|
|
}
|