Scope alarm tracking to selected templates and surface endpoint/security state on the dashboard so operators can deploy in large galaxies without drowning clients in irrelevant alarms or guessing what the server is advertising
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// End-to-end integration tests that boot a real LmxNodeManager against fake Galaxy data and verify
|
||||
/// the template-based alarm object filter actually suppresses alarm condition creation in both the
|
||||
/// full build path and the subtree rebuild path after a simulated Galaxy redeploy.
|
||||
/// </summary>
|
||||
public class AlarmObjectFilterIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Filter_Empty_AllAlarmsTracked()
|
||||
{
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
repo: CreateRepoWithMixedTemplates(),
|
||||
alarmTrackingEnabled: true);
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
fixture.NodeManager.ShouldNotBeNull();
|
||||
// Two alarm attributes total (one per object), no filter → both tracked.
|
||||
fixture.NodeManager!.AlarmConditionCount.ShouldBe(2);
|
||||
fixture.NodeManager.AlarmFilterEnabled.ShouldBeFalse();
|
||||
fixture.NodeManager.AlarmFilterIncludedObjectCount.ShouldBe(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_MatchesOneTemplate_OnlyMatchingAlarmTracked()
|
||||
{
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
repo: CreateRepoWithMixedTemplates(),
|
||||
alarmTrackingEnabled: true,
|
||||
alarmObjectFilters: new[] { "TestMachine*" });
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
fixture.NodeManager.ShouldNotBeNull();
|
||||
fixture.NodeManager!.AlarmFilterEnabled.ShouldBeTrue();
|
||||
fixture.NodeManager.AlarmFilterPatternCount.ShouldBe(1);
|
||||
fixture.NodeManager.AlarmConditionCount.ShouldBe(1);
|
||||
fixture.NodeManager.AlarmFilterIncludedObjectCount.ShouldBe(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_MatchesParent_PropagatesToChild()
|
||||
{
|
||||
var attrs = new List<GalaxyAttributeInfo>();
|
||||
attrs.AddRange(AlarmWithInAlarm(1, "Parent_001", "AlarmA"));
|
||||
attrs.AddRange(AlarmWithInAlarm(2, "Child_001", "AlarmB"));
|
||||
|
||||
var repo = new FakeGalaxyRepository
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
Obj(1, parent: 0, tag: "Parent_001", template: "TestMachine"),
|
||||
Obj(2, parent: 1, tag: "Child_001", template: "UnrelatedWidget")
|
||||
},
|
||||
Attributes = attrs
|
||||
};
|
||||
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
repo: repo,
|
||||
alarmTrackingEnabled: true,
|
||||
alarmObjectFilters: new[] { "TestMachine*" });
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
fixture.NodeManager!.AlarmConditionCount.ShouldBe(2);
|
||||
fixture.NodeManager.AlarmFilterIncludedObjectCount.ShouldBe(2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_NoMatch_ZeroAlarmConditions()
|
||||
{
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
repo: CreateRepoWithMixedTemplates(),
|
||||
alarmTrackingEnabled: true,
|
||||
alarmObjectFilters: new[] { "NotInGalaxy*" });
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
fixture.NodeManager!.AlarmConditionCount.ShouldBe(0);
|
||||
fixture.NodeManager.AlarmFilterIncludedObjectCount.ShouldBe(0);
|
||||
fixture.NodeManager.AlarmFilterPatternCount.ShouldBe(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_GalaxyDollarPrefix_Normalized()
|
||||
{
|
||||
// Template chain stored as "$TestMachine" must match operator pattern "TestMachine*".
|
||||
var repo = new FakeGalaxyRepository
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
Obj(1, parent: 0, tag: "Obj_1", template: "$TestMachine")
|
||||
},
|
||||
Attributes = new List<GalaxyAttributeInfo>(AlarmWithInAlarm(1, "Obj_1", "AlarmX"))
|
||||
};
|
||||
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
|
||||
repo: repo,
|
||||
alarmTrackingEnabled: true,
|
||||
alarmObjectFilters: new[] { "TestMachine*" });
|
||||
await fixture.InitializeAsync();
|
||||
try
|
||||
{
|
||||
fixture.NodeManager!.AlarmConditionCount.ShouldBe(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Helpers ----------
|
||||
|
||||
private static FakeGalaxyRepository CreateRepoWithMixedTemplates()
|
||||
{
|
||||
var attrs = new List<GalaxyAttributeInfo>();
|
||||
attrs.AddRange(AlarmWithInAlarm(1, "TestMachine_001", "MachineAlarm"));
|
||||
attrs.AddRange(AlarmWithInAlarm(2, "Pump_001", "PumpAlarm"));
|
||||
return new FakeGalaxyRepository
|
||||
{
|
||||
Hierarchy = new List<GalaxyObjectInfo>
|
||||
{
|
||||
Obj(1, parent: 0, tag: "TestMachine_001", template: "TestMachine"),
|
||||
Obj(2, parent: 0, tag: "Pump_001", template: "Pump")
|
||||
},
|
||||
Attributes = attrs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a Galaxy alarm attribute plus its companion <c>.InAlarm</c> sub-attribute. The alarm
|
||||
/// creation path in LmxNodeManager skips objects whose alarm attribute has no InAlarm variable
|
||||
/// in the tag→node map, so tests must include both rows for alarm conditions to materialize.
|
||||
/// </summary>
|
||||
private static IEnumerable<GalaxyAttributeInfo> AlarmWithInAlarm(int gobjectId, string tag, string attrName)
|
||||
{
|
||||
yield return new GalaxyAttributeInfo
|
||||
{
|
||||
GobjectId = gobjectId,
|
||||
TagName = tag,
|
||||
AttributeName = attrName,
|
||||
FullTagReference = $"{tag}.{attrName}",
|
||||
MxDataType = 1,
|
||||
IsAlarm = true
|
||||
};
|
||||
yield return new GalaxyAttributeInfo
|
||||
{
|
||||
GobjectId = gobjectId,
|
||||
TagName = tag,
|
||||
AttributeName = attrName + ".InAlarm",
|
||||
FullTagReference = $"{tag}.{attrName}.InAlarm",
|
||||
MxDataType = 1,
|
||||
IsAlarm = false
|
||||
};
|
||||
}
|
||||
|
||||
private static GalaxyObjectInfo Obj(int id, int parent, string tag, string template) => new()
|
||||
{
|
||||
GobjectId = id,
|
||||
ParentGobjectId = parent,
|
||||
TagName = tag,
|
||||
ContainedName = tag,
|
||||
BrowseName = tag,
|
||||
IsArea = false,
|
||||
TemplateChain = new List<string> { template }
|
||||
};
|
||||
|
||||
private static GalaxyAttributeInfo AlarmAttr(int gobjectId, string tag, string attrName) => new()
|
||||
{
|
||||
GobjectId = gobjectId,
|
||||
TagName = tag,
|
||||
AttributeName = attrName,
|
||||
FullTagReference = $"{tag}.{attrName}",
|
||||
MxDataType = 1,
|
||||
IsAlarm = true
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user