test(alarms): cover isNative rebuild/kind-flip lifecycle + Phase7Applier call-site (code-review)
This commit is contained in:
@@ -354,6 +354,56 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
|||||||
await host.DisposeAsync();
|
await host.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>H6a lifecycle — the native flag is NOT sticky across a rebuild + kind flip. Materialise an
|
||||||
|
/// id as native (flag true), <see cref="OtOpcUaNodeManager.RebuildAddressSpace"/> (which clears the
|
||||||
|
/// folder + condition + native-flag sets), re-ensure the equipment folder, then re-materialise the
|
||||||
|
/// SAME id as scripted (<c>isNative:false</c>). The flag must read false — the rebuild dropped it and
|
||||||
|
/// the scripted re-materialise did NOT re-add it. Guards against a stale-native leak that would route a
|
||||||
|
/// now-scripted alarm's inbound ack to the driver.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Native_flag_clears_on_rebuild_then_kind_flip()
|
||||||
|
{
|
||||||
|
var (host, server) = await BootAsync();
|
||||||
|
var nm = server.NodeManager!;
|
||||||
|
|
||||||
|
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||||
|
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
|
||||||
|
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
|
||||||
|
|
||||||
|
// RebuildAddressSpace clears the folder set too, so the equipment folder must be re-ensured
|
||||||
|
// before the same id can be re-materialised (ResolveParentFolder needs the parent back).
|
||||||
|
nm.RebuildAddressSpace();
|
||||||
|
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||||
|
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false);
|
||||||
|
|
||||||
|
nm.IsNativeAlarmNode("a1").ShouldBeFalse();
|
||||||
|
|
||||||
|
await host.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>H6a lifecycle (converse) — a scripted condition can flip TO native across a rebuild.
|
||||||
|
/// Materialise an id as scripted (flag false), rebuild, re-ensure the folder, re-materialise the SAME
|
||||||
|
/// id as native (<c>isNative:true</c>). The flag must read true — the native re-materialise re-adds it
|
||||||
|
/// cleanly after the rebuild cleared the slate.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Scripted_flag_can_flip_to_native_across_rebuild()
|
||||||
|
{
|
||||||
|
var (host, server) = await BootAsync();
|
||||||
|
var nm = server.NodeManager!;
|
||||||
|
|
||||||
|
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||||
|
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false);
|
||||||
|
nm.IsNativeAlarmNode("a1").ShouldBeFalse();
|
||||||
|
|
||||||
|
nm.RebuildAddressSpace();
|
||||||
|
nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
|
||||||
|
nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
|
||||||
|
|
||||||
|
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
|
||||||
|
|
||||||
|
await host.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Builds a <see cref="ServerSystemContext"/> (an <see cref="ISessionOperationContext"/>)
|
/// <summary>Builds a <see cref="ServerSystemContext"/> (an <see cref="ISessionOperationContext"/>)
|
||||||
/// carrying a <see cref="RoleCarryingUserIdentity"/> with the given name + roles — the exact seam the
|
/// carrying a <see cref="RoleCarryingUserIdentity"/> with the given name + roles — the exact seam the
|
||||||
/// gate reads via <c>(context as ISessionOperationContext)?.UserIdentity as RoleCarryingUserIdentity</c>.</summary>
|
/// gate reads via <c>(context as ISessionOperationContext)?.UserIdentity as RoleCarryingUserIdentity</c>.</summary>
|
||||||
|
|||||||
@@ -228,8 +228,9 @@ public sealed class Phase7ApplierTests
|
|||||||
// The alarm tag drove MaterialiseAlarmCondition (folder-scoped NodeId, equipment parent,
|
// The alarm tag drove MaterialiseAlarmCondition (folder-scoped NodeId, equipment parent,
|
||||||
// matching display/type/severity) and did NOT drive EnsureVariable.
|
// matching display/type/severity) and did NOT drive EnsureVariable.
|
||||||
var alarmNodeId = EquipmentNodeIds.Variable("eq-1", "", "OverTemp");
|
var alarmNodeId = EquipmentNodeIds.Variable("eq-1", "", "OverTemp");
|
||||||
|
// A native equipment-tag alarm: the call-site threads isNative: true.
|
||||||
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
||||||
.ShouldBe((alarmNodeId, "eq-1", "OverTemp", "OffNormalAlarm", 700));
|
.ShouldBe((alarmNodeId, "eq-1", "OverTemp", "OffNormalAlarm", 700, true));
|
||||||
sink.VariableCalls.ShouldNotContain(v => v.NodeId == alarmNodeId);
|
sink.VariableCalls.ShouldNotContain(v => v.NodeId == alarmNodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,8 +258,9 @@ public sealed class Phase7ApplierTests
|
|||||||
sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("eq-1/Diagnostics", "eq-1", "Diagnostics"));
|
sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("eq-1/Diagnostics", "eq-1", "Diagnostics"));
|
||||||
// Condition is parented to the sub-folder, with the folder-scoped NodeId. No value variable.
|
// Condition is parented to the sub-folder, with the folder-scoped NodeId. No value variable.
|
||||||
var alarmNodeId = EquipmentNodeIds.Variable("eq-1", "Diagnostics", "OverTemp");
|
var alarmNodeId = EquipmentNodeIds.Variable("eq-1", "Diagnostics", "OverTemp");
|
||||||
|
// A native equipment-tag alarm (with a FolderPath): the call-site still threads isNative: true.
|
||||||
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
||||||
.ShouldBe((alarmNodeId, "eq-1/Diagnostics", "OverTemp", "OffNormalAlarm", 500));
|
.ShouldBe((alarmNodeId, "eq-1/Diagnostics", "OverTemp", "OffNormalAlarm", 500, true));
|
||||||
sink.VariableCalls.ShouldBeEmpty();
|
sink.VariableCalls.ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,8 +447,9 @@ public sealed class Phase7ApplierTests
|
|||||||
applier.MaterialiseScriptedAlarms(composition);
|
applier.MaterialiseScriptedAlarms(composition);
|
||||||
|
|
||||||
// Only the enabled alarm is materialised; the disabled one is skipped entirely.
|
// Only the enabled alarm is materialised; the disabled one is skipped entirely.
|
||||||
|
// A SCRIPTED alarm: the call-site threads isNative: false (guards against a native/scripted swap).
|
||||||
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
||||||
.ShouldBe(("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700));
|
.ShouldBe(("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that added equipment tags in an otherwise-empty plan trigger an
|
/// <summary>Verifies that added equipment tags in an otherwise-empty plan trigger an
|
||||||
@@ -717,7 +720,7 @@ public sealed class Phase7ApplierTests
|
|||||||
/// keyed by NodeId (null ⇒ that call passed not-historized).</summary>
|
/// keyed by NodeId (null ⇒ that call passed not-historized).</summary>
|
||||||
public ConcurrentQueue<(string NodeId, string? HistorianTagname)> HistorianQueue { get; } = new();
|
public ConcurrentQueue<(string NodeId, string? HistorianTagname)> HistorianQueue { get; } = new();
|
||||||
/// <summary>Gets the queue of alarm-condition materialise calls.</summary>
|
/// <summary>Gets the queue of alarm-condition materialise calls.</summary>
|
||||||
public ConcurrentQueue<(string AlarmNodeId, string EquipmentNodeId, string DisplayName, string AlarmType, int Severity)> AlarmConditionQueue { get; } = new();
|
public ConcurrentQueue<(string AlarmNodeId, string EquipmentNodeId, string DisplayName, string AlarmType, int Severity, bool IsNative)> AlarmConditionQueue { get; } = new();
|
||||||
/// <summary>Gets the number of rebuild calls made on this sink.</summary>
|
/// <summary>Gets the number of rebuild calls made on this sink.</summary>
|
||||||
public int RebuildCalls;
|
public int RebuildCalls;
|
||||||
|
|
||||||
@@ -730,7 +733,7 @@ public sealed class Phase7ApplierTests
|
|||||||
/// <summary>Gets the list of recorded (NodeId, historian-tagname) pairs captured per EnsureVariable call.</summary>
|
/// <summary>Gets the list of recorded (NodeId, historian-tagname) pairs captured per EnsureVariable call.</summary>
|
||||||
public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
|
public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
|
||||||
/// <summary>Gets the list of recorded alarm-condition materialise calls.</summary>
|
/// <summary>Gets the list of recorded alarm-condition materialise calls.</summary>
|
||||||
public List<(string AlarmNodeId, string EquipmentNodeId, string DisplayName, string AlarmType, int Severity)> AlarmConditionCalls => AlarmConditionQueue.ToList();
|
public List<(string AlarmNodeId, string EquipmentNodeId, string DisplayName, string AlarmType, int Severity, bool IsNative)> AlarmConditionCalls => AlarmConditionQueue.ToList();
|
||||||
|
|
||||||
/// <summary>Records a value write (no-op in this recording sink).</summary>
|
/// <summary>Records a value write (no-op in this recording sink).</summary>
|
||||||
/// <param name="nodeId">The node ID.</param>
|
/// <param name="nodeId">The node ID.</param>
|
||||||
@@ -751,7 +754,7 @@ public sealed class Phase7ApplierTests
|
|||||||
/// <param name="alarmType">The domain alarm type.</param>
|
/// <param name="alarmType">The domain alarm type.</param>
|
||||||
/// <param name="severity">The domain severity.</param>
|
/// <param name="severity">The domain severity.</param>
|
||||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||||
=> AlarmConditionQueue.Enqueue((alarmNodeId, equipmentNodeId, displayName, alarmType, severity));
|
=> AlarmConditionQueue.Enqueue((alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative));
|
||||||
/// <summary>Records a folder creation call.</summary>
|
/// <summary>Records a folder creation call.</summary>
|
||||||
/// <param name="folderNodeId">The folder node ID.</param>
|
/// <param name="folderNodeId">The folder node ID.</param>
|
||||||
/// <param name="parentNodeId">The parent folder node ID, if any.</param>
|
/// <param name="parentNodeId">The parent folder node ID, if any.</param>
|
||||||
|
|||||||
Reference in New Issue
Block a user