Merge main (DCL alarm fixes 06ef177..9b78e60) into M3 branch
This commit is contained in:
+36
@@ -195,4 +195,40 @@ public class DataConnectionActorAlarmTests : TestKit
|
||||
"", "", "", "", "", null, DateTimeOffset.UtcNow, "", ""));
|
||||
ExpectMsg<NativeAlarmTransitionUpdate>(u => u.Transition.Kind == AlarmTransitionKind.SnapshotComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubscribeAlarms_RealEmptyRefSnapshotComplete_IsBroadcastToSpecificSource()
|
||||
{
|
||||
// Regression: the real MxGatewayAlarmMapper.SnapshotComplete() emits the
|
||||
// sentinel with EMPTY SourceReference / SourceObjectReference. With a
|
||||
// specific (prefix) source like "Reactor." the per-source match
|
||||
// ("".StartsWith("Reactor.") == false) used to drop it, stranding the
|
||||
// buffered snapshot in the NativeAlarmActor forever — statically-active
|
||||
// conditions (delivered only in the snapshot) never surfaced. The sentinel
|
||||
// must be broadcast to every subscriber so the snapshot swap completes.
|
||||
var (adapter, getCb) = BuildAlarmAdapter();
|
||||
var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor(
|
||||
"conn", adapter, _options, _health, _factory, "MxGateway")));
|
||||
|
||||
actor.Tell(new SubscribeAlarmsRequest("c", "inst", "conn", "Reactor.", null, DateTimeOffset.UtcNow));
|
||||
ExpectMsg<SubscribeAlarmsResponse>(m => m.Success);
|
||||
var cb = getCb();
|
||||
Assert.NotNull(cb);
|
||||
|
||||
// Active alarm under the source arrives in the snapshot and routes by prefix.
|
||||
cb!(new NativeAlarmTransition(
|
||||
"Galaxy!Area.Reactor.HeartbeatTimeoutAlarm", "Reactor.HeartbeatTimeoutAlarm", "Syst",
|
||||
AlarmTransitionKind.Snapshot,
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 400),
|
||||
"Area", "", "", "", "", null, DateTimeOffset.UtcNow, "", ""));
|
||||
ExpectMsg<NativeAlarmTransitionUpdate>(u =>
|
||||
u.Transition.Kind == AlarmTransitionKind.Snapshot &&
|
||||
u.Transition.SourceObjectReference == "Reactor.HeartbeatTimeoutAlarm");
|
||||
|
||||
// Real sentinel with EMPTY refs — must still reach the "Reactor." subscriber.
|
||||
cb!(new NativeAlarmTransition("", "", "", AlarmTransitionKind.SnapshotComplete,
|
||||
new AlarmConditionState(false, true, null, AlarmShelveState.Unshelved, false, 0),
|
||||
"", "", "", "", "", null, DateTimeOffset.UtcNow, "", ""));
|
||||
ExpectMsg<NativeAlarmTransitionUpdate>(u => u.Transition.Kind == AlarmTransitionKind.SnapshotComplete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +546,43 @@ public class DataConnectionActorTests : TestKit
|
||||
Assert.True(ack.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DCL026_StaticTagSeedValue_IsDeliveredAfterRegistration()
|
||||
{
|
||||
// Regression test for DataConnectionLayer-026. The initial-read seed value used to
|
||||
// be emitted (TagValueReceived) from HandleSubscribe's background task BEFORE
|
||||
// HandleSubscribeCompleted registered the instance's tags in
|
||||
// _subscriptionsByInstance. HandleTagValueReceived's fan-out then found no
|
||||
// subscriber for the tag and silently dropped the value. A tag that soon gets a
|
||||
// real data-change notification recovers, but a STATIC tag (subscribe succeeds,
|
||||
// callback never fires again — e.g. an idle MES field) was left Uncertain forever.
|
||||
// After the fix the seed rides on SubscribeCompleted and is delivered AFTER
|
||||
// registration, so the subscriber receives it.
|
||||
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.CompletedTask);
|
||||
_mockAdapter.Status.Returns(ConnectionHealth.Connected);
|
||||
// Subscribe succeeds; the adapter never invokes the value callback (a static tag).
|
||||
_mockAdapter.SubscribeAsync(Arg.Any<string>(), Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>())
|
||||
.Returns(_ => Task.FromResult("sub-static"));
|
||||
// The gateway returns a Good current value for the static tag.
|
||||
_mockAdapter.ReadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ReadResult(true,
|
||||
new TagValue("Left54321", QualityCode.Good, DateTimeOffset.UtcNow), null));
|
||||
|
||||
var actor = CreateConnectionActor("dcl026-static-seed");
|
||||
await Task.Delay(300); // reach Connected state
|
||||
|
||||
actor.Tell(new SubscribeTagsRequest(
|
||||
"c1", "inst1", "dcl026-static-seed",
|
||||
["MESReceiver_023.MoveInMesContainerNum"], DateTimeOffset.UtcNow));
|
||||
|
||||
// The seeded value must reach the subscriber (was dropped pre-fix).
|
||||
var update = FishForMessage<TagValueUpdate>(_ => true, TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("MESReceiver_023.MoveInMesContainerNum", update.TagPath);
|
||||
Assert.Equal(QualityCode.Good, update.Quality);
|
||||
Assert.Equal("Left54321", update.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DCL004_ConnectionLevelSubscribeFailure_TriggersReconnect_NotTagRetry()
|
||||
{
|
||||
|
||||
@@ -65,6 +65,54 @@ public class MxGatewayAlarmMapperTests
|
||||
Assert.Equal(1000, t.Condition.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceReference_IsObjectRelative_NotFullProviderReference()
|
||||
{
|
||||
// The condition identity surfaced upward is the object-relative reference
|
||||
// (e.g. "Z28061.HeartbeatTimeoutAlarm"), not the gateway's full provider
|
||||
// reference ("Galaxy!<area>.<object>.<alarm>"). Area lives in Category.
|
||||
var snap = new ActiveAlarmSnapshot
|
||||
{
|
||||
AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
|
||||
SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
|
||||
AlarmTypeName = "Syst",
|
||||
Category = "CVDAisle_1",
|
||||
CurrentState = ProtoConditionState.Active,
|
||||
Severity = 400
|
||||
};
|
||||
var ev = new OnAlarmTransitionEvent
|
||||
{
|
||||
AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
|
||||
SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
|
||||
AlarmTypeName = "Syst",
|
||||
TransitionKind = ProtoTransitionKind.Raise,
|
||||
Severity = 400
|
||||
};
|
||||
|
||||
var snapT = MxGatewayAlarmMapper.MapSnapshot(snap);
|
||||
var liveT = MxGatewayAlarmMapper.MapTransition(ev);
|
||||
|
||||
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", snapT.SourceReference);
|
||||
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", liveT.SourceReference);
|
||||
Assert.Equal("CVDAisle_1", snapT.Category);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceReference_FallsBackToFullReference_WhenObjectReferenceEmpty()
|
||||
{
|
||||
var snap = new ActiveAlarmSnapshot
|
||||
{
|
||||
AlarmFullReference = "Galaxy!Area.Obj.Alarm",
|
||||
SourceObjectReference = "",
|
||||
CurrentState = ProtoConditionState.Active,
|
||||
Severity = 100
|
||||
};
|
||||
|
||||
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
|
||||
|
||||
Assert.Equal("Galaxy!Area.Obj.Alarm", t.SourceReference);
|
||||
}
|
||||
|
||||
// ── CurrentValue / LimitValue (M2.13 / #27) ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user