513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
168 lines
7.3 KiB
C#
168 lines
7.3 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.Instance;
|
|
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;
|
|
using ZB.MOM.WW.ScadaBridge.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
|
|
|
/// <summary>
|
|
/// The deployed alarm's <c>IsMesReportable</c> flag must reach the site-local alarm
|
|
/// snapshot that <c>Alarms.CurrentAsync()</c> and the Debug View query both build (the
|
|
/// flag does not cross <c>DebugAlarmStateDto</c> to central in this slice). It is stamped in ONE
|
|
/// place — <c>BuildAlarmStatesSnapshot</c> — so a computed alarm that has fired (served
|
|
/// from the retained <c>AlarmStateChanged</c>) and one that never has (served from the
|
|
/// bare Normal fallback) can never disagree about the flag.
|
|
/// </summary>
|
|
public class InstanceActorMesReportableSnapshotTests : TestKit, IDisposable
|
|
{
|
|
private readonly SiteStorageService _storage;
|
|
private readonly ScriptCompilationService _compilationService;
|
|
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
|
private readonly SiteRuntimeOptions _options = new();
|
|
private readonly TestLocalDb _localDb;
|
|
|
|
/// <summary>Initializes the test class with a temporary storage setup.</summary>
|
|
public InstanceActorMesReportableSnapshotTests()
|
|
{
|
|
_localDb = TestLocalDb.CreateTemp("instance-mes-reportable");
|
|
_storage = new SiteStorageService(_localDb.Db, NullLogger<SiteStorageService>.Instance);
|
|
_storage.InitializeAsync().GetAwaiter().GetResult();
|
|
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
|
_sharedScriptLibrary = new SharedScriptLibrary(_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
|
}
|
|
|
|
private IActorRef CreateInstanceActor(string instanceName, FlattenedConfiguration config) =>
|
|
ActorOf(Props.Create(() => new InstanceActor(
|
|
instanceName,
|
|
JsonSerializer.Serialize(config),
|
|
_storage,
|
|
_compilationService,
|
|
_sharedScriptLibrary,
|
|
null,
|
|
_options,
|
|
NullLogger<InstanceActor>.Instance,
|
|
null)));
|
|
|
|
/// <summary>
|
|
/// One static attribute driving two computed alarms: <c>HighTemp</c> is flagged
|
|
/// MES-reportable and fires at 80; <c>TempSpike</c> is not flagged and only matches
|
|
/// the exact value 100, so it never fires in this test.
|
|
/// </summary>
|
|
private static FlattenedConfiguration TwoComputedAlarmsConfig(string instanceName) => new()
|
|
{
|
|
InstanceUniqueName = instanceName,
|
|
Attributes =
|
|
[
|
|
new ResolvedAttribute
|
|
{
|
|
CanonicalName = "Temperature",
|
|
Value = "20",
|
|
DataType = "Double"
|
|
}
|
|
],
|
|
Alarms =
|
|
[
|
|
new ResolvedAlarm
|
|
{
|
|
CanonicalName = "HighTemp",
|
|
TriggerType = "HiLo",
|
|
TriggerConfiguration = "{\"attributeName\":\"Temperature\",\"hi\":80}",
|
|
PriorityLevel = 700,
|
|
IsMesReportable = true
|
|
},
|
|
new ResolvedAlarm
|
|
{
|
|
CanonicalName = "TempSpike",
|
|
TriggerType = "ValueMatch",
|
|
TriggerConfiguration = "{\"attributeName\":\"Temperature\",\"matchValue\":\"100\"}",
|
|
PriorityLevel = 300,
|
|
IsMesReportable = false
|
|
}
|
|
]
|
|
};
|
|
|
|
private GetAlarmSnapshotResponse AskSnapshot(IActorRef actor, string instanceName)
|
|
{
|
|
var correlationId = Guid.NewGuid().ToString();
|
|
actor.Tell(new GetAlarmSnapshotRequest(correlationId, instanceName, DateTimeOffset.UtcNow));
|
|
// Match on the correlation id so a late reply from a timed-out probe can never be
|
|
// consumed as the "current" snapshot by a later call; keep the inner wait well
|
|
// inside the AwaitAssert budget so one slow probe doesn't eat the whole retry window.
|
|
return ExpectMsg<GetAlarmSnapshotResponse>(
|
|
m => m.CorrelationId == correlationId, TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
/// <summary>Verifies that the alarm snapshot stamps the MES-reportable flag on both fired and unfired alarms.</summary>
|
|
[Fact]
|
|
public void AlarmSnapshot_StampsIsMesReportable_OnFiredAndUnfiredAlarms()
|
|
{
|
|
const string instance = "Oven1";
|
|
var actor = CreateInstanceActor(instance, TwoComputedAlarmsConfig(instance));
|
|
|
|
// Drive Temperature above the Hi setpoint: HighTemp fires, TempSpike does not.
|
|
actor.Tell(new SetStaticAttributeCommand(
|
|
"corr-temp", instance, "Temperature", "90", DateTimeOffset.UtcNow));
|
|
var write = ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5));
|
|
Assert.True(write.Success);
|
|
|
|
// The alarm child evaluates asynchronously — poll the snapshot until the
|
|
// retained event for HighTemp has landed.
|
|
AwaitAssert(() =>
|
|
{
|
|
var pending = AskSnapshot(actor, instance);
|
|
var firing = Assert.Single(pending.Alarms, a => a.AlarmName == "HighTemp");
|
|
Assert.Equal(AlarmState.Active, firing.State);
|
|
}, TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(100));
|
|
|
|
var snapshot = AskSnapshot(actor, instance);
|
|
|
|
// Both rows are still returned — stamping must not drop or duplicate anything.
|
|
Assert.Equal(2, snapshot.Alarms.Count);
|
|
|
|
// Fired alarm: served from the retained AlarmStateChanged.
|
|
var fired = Assert.Single(snapshot.Alarms, a => a.AlarmName == "HighTemp");
|
|
Assert.Equal(AlarmState.Active, fired.State);
|
|
Assert.True(fired.IsMesReportable);
|
|
|
|
// Never-fired alarm: served from the bare Normal fallback projection.
|
|
var unfired = Assert.Single(snapshot.Alarms, a => a.AlarmName == "TempSpike");
|
|
Assert.Equal(AlarmState.Normal, unfired.State);
|
|
Assert.False(unfired.IsMesReportable);
|
|
}
|
|
|
|
/// <summary>Verifies that an unfired reportable alarm is stamped before it ever fires.</summary>
|
|
[Fact]
|
|
public void AlarmSnapshot_UnfiredReportableAlarm_IsStampedBeforeItEverFires()
|
|
{
|
|
// The fallback branch must carry the flag on its own: an alarm that has never
|
|
// emitted an event is still MES-reportable per its deployed configuration.
|
|
const string instance = "Oven2";
|
|
var actor = CreateInstanceActor(instance, TwoComputedAlarmsConfig(instance));
|
|
|
|
var snapshot = AskSnapshot(actor, instance);
|
|
|
|
Assert.Equal(2, snapshot.Alarms.Count);
|
|
Assert.True(Assert.Single(snapshot.Alarms, a => a.AlarmName == "HighTemp").IsMesReportable);
|
|
Assert.False(Assert.Single(snapshot.Alarms, a => a.AlarmName == "TempSpike").IsMesReportable);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
void IDisposable.Dispose()
|
|
{
|
|
// TestKit teardown first, so no in-flight actor can reach a disposed ILocalDb;
|
|
// then dispose the database before deleting — the master connection anchors the WAL.
|
|
Shutdown();
|
|
var path = _localDb.Path;
|
|
_localDb.Dispose();
|
|
TestLocalDb.DeleteFiles(path);
|
|
}
|
|
}
|