191 lines
7.1 KiB
C#
191 lines
7.1 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Threading.Tasks;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Alarms;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class GalaxyAlarmTrackerTests
|
|
{
|
|
private sealed class FakeSubscriber
|
|
{
|
|
public readonly ConcurrentDictionary<string, Action<string, Vtq>> Subs = new();
|
|
public readonly ConcurrentQueue<string> Unsubs = new();
|
|
public readonly ConcurrentQueue<(string Tag, object Value)> Writes = new();
|
|
public bool WriteReturns { get; set; } = true;
|
|
|
|
public Task Subscribe(string tag, Action<string, Vtq> cb)
|
|
{
|
|
Subs[tag] = cb;
|
|
return Task.CompletedTask;
|
|
}
|
|
public Task Unsubscribe(string tag)
|
|
{
|
|
Unsubs.Enqueue(tag);
|
|
Subs.TryRemove(tag, out _);
|
|
return Task.CompletedTask;
|
|
}
|
|
public Task<bool> Write(string tag, object value)
|
|
{
|
|
Writes.Enqueue((tag, value));
|
|
return Task.FromResult(WriteReturns);
|
|
}
|
|
}
|
|
|
|
private static Vtq Bool(bool v) => new(v, DateTime.UtcNow, 192);
|
|
private static Vtq Int(int v) => new(v, DateTime.UtcNow, 192);
|
|
private static Vtq Str(string v) => new(v, DateTime.UtcNow, 192);
|
|
|
|
[Fact]
|
|
public async Task Track_subscribes_to_four_alarm_attributes()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
|
|
await t.TrackAsync("Tank.Level.HiHi");
|
|
|
|
fake.Subs.ShouldContainKey("Tank.Level.HiHi.InAlarm");
|
|
fake.Subs.ShouldContainKey("Tank.Level.HiHi.Priority");
|
|
fake.Subs.ShouldContainKey("Tank.Level.HiHi.DescAttrName");
|
|
fake.Subs.ShouldContainKey("Tank.Level.HiHi.Acked");
|
|
t.TrackedAlarmCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Track_is_idempotent_on_repeat_call()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
|
|
await t.TrackAsync("Alarm.A");
|
|
await t.TrackAsync("Alarm.A");
|
|
|
|
t.TrackedAlarmCount.ShouldBe(1);
|
|
fake.Subs.Count.ShouldBe(4); // 4 sub calls, not 8
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InAlarm_false_to_true_fires_Active_transition()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
var transitions = new ConcurrentQueue<AlarmTransition>();
|
|
t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);
|
|
|
|
await t.TrackAsync("Alarm.A");
|
|
fake.Subs["Alarm.A.Priority"]("Alarm.A.Priority", Int(500));
|
|
fake.Subs["Alarm.A.DescAttrName"]("Alarm.A.DescAttrName", Str("TankLevelHiHi"));
|
|
fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));
|
|
|
|
transitions.Count.ShouldBe(1);
|
|
transitions.TryDequeue(out var tr).ShouldBeTrue();
|
|
tr!.Transition.ShouldBe(AlarmStateTransition.Active);
|
|
tr.Priority.ShouldBe(500);
|
|
tr.DescAttrName.ShouldBe("TankLevelHiHi");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InAlarm_true_to_false_fires_Inactive_transition()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
var transitions = new ConcurrentQueue<AlarmTransition>();
|
|
t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);
|
|
|
|
await t.TrackAsync("Alarm.A");
|
|
fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));
|
|
fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(false));
|
|
|
|
transitions.Count.ShouldBe(2);
|
|
transitions.TryDequeue(out _);
|
|
transitions.TryDequeue(out var tr).ShouldBeTrue();
|
|
tr!.Transition.ShouldBe(AlarmStateTransition.Inactive);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acked_false_to_true_fires_Acknowledged_while_InAlarm_is_true()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
var transitions = new ConcurrentQueue<AlarmTransition>();
|
|
t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);
|
|
|
|
await t.TrackAsync("Alarm.A");
|
|
fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true)); // Active, clears Acked flag
|
|
fake.Subs["Alarm.A.Acked"]("Alarm.A.Acked", Bool(true)); // Acknowledged
|
|
|
|
transitions.Count.ShouldBe(2);
|
|
transitions.TryDequeue(out _);
|
|
transitions.TryDequeue(out var tr).ShouldBeTrue();
|
|
tr!.Transition.ShouldBe(AlarmStateTransition.Acknowledged);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acked_transition_while_InAlarm_is_false_does_not_fire()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
var transitions = new ConcurrentQueue<AlarmTransition>();
|
|
t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);
|
|
|
|
await t.TrackAsync("Alarm.A");
|
|
// Initial Acked=true on subscribe (alarm is at rest, pre-ack'd) — should not fire.
|
|
fake.Subs["Alarm.A.Acked"]("Alarm.A.Acked", Bool(true));
|
|
|
|
transitions.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Acknowledge_writes_AckMsg_with_comment()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
await t.TrackAsync("Alarm.A");
|
|
|
|
var ok = await t.AcknowledgeAsync("Alarm.A", "acknowledged by operator");
|
|
|
|
ok.ShouldBeTrue();
|
|
fake.Writes.Count.ShouldBe(1);
|
|
fake.Writes.TryDequeue(out var w).ShouldBeTrue();
|
|
w.Tag.ShouldBe("Alarm.A.AckMsg");
|
|
w.Value.ShouldBe("acknowledged by operator");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Snapshot_reports_latest_fields()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
await t.TrackAsync("Alarm.A");
|
|
fake.Subs["Alarm.A.InAlarm"]("Alarm.A.InAlarm", Bool(true));
|
|
fake.Subs["Alarm.A.Priority"]("Alarm.A.Priority", Int(900));
|
|
fake.Subs["Alarm.A.DescAttrName"]("Alarm.A.DescAttrName", Str("MyAlarm"));
|
|
fake.Subs["Alarm.A.Acked"]("Alarm.A.Acked", Bool(true));
|
|
|
|
var snap = t.SnapshotStates();
|
|
snap.Count.ShouldBe(1);
|
|
snap[0].InAlarm.ShouldBeTrue();
|
|
snap[0].Acked.ShouldBeTrue();
|
|
snap[0].Priority.ShouldBe(900);
|
|
snap[0].DescAttrName.ShouldBe("MyAlarm");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Foreign_probe_callback_is_dropped()
|
|
{
|
|
var fake = new FakeSubscriber();
|
|
using var t = new GalaxyAlarmTracker(fake.Subscribe, fake.Unsubscribe, fake.Write);
|
|
var transitions = new ConcurrentQueue<AlarmTransition>();
|
|
t.TransitionRaised += (_, tr) => transitions.Enqueue(tr);
|
|
|
|
// No TrackAsync was called — this callback is foreign and should be silently ignored.
|
|
t.OnProbeCallback("Unknown.InAlarm", Bool(true));
|
|
|
|
transitions.Count.ShouldBe(0);
|
|
}
|
|
}
|