server(alarms): watch-list resolver merging GR discovery + config override
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Alarms;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AlarmWatchListResolver"/>: discovery/config merge,
|
||||
/// subtag-address composition, canonical reference shaping, and the
|
||||
/// unavailable-discovery code path.
|
||||
/// </summary>
|
||||
public sealed class AlarmWatchListResolverTests
|
||||
{
|
||||
private static AlarmWatchListResolver CreateResolver(IGalaxyRepository repository) =>
|
||||
new(repository, NullLogger<AlarmWatchListResolver>.Instance);
|
||||
|
||||
private static AlarmsOptions Options(
|
||||
bool useGalaxyRepository = true,
|
||||
string area = "",
|
||||
string defaultArea = "",
|
||||
string[]? include = null,
|
||||
string[]? exclude = null,
|
||||
AlarmSubtagNameOptions? subtags = null) =>
|
||||
new()
|
||||
{
|
||||
DefaultArea = defaultArea,
|
||||
Fallback = new AlarmFallbackOptions
|
||||
{
|
||||
Discovery = new AlarmDiscoveryOptions
|
||||
{
|
||||
UseGalaxyRepository = useGalaxyRepository,
|
||||
Area = area,
|
||||
IncludeAttributes = include ?? [],
|
||||
ExcludeAttributes = exclude ?? [],
|
||||
},
|
||||
Subtags = subtags ?? new AlarmSubtagNameOptions(),
|
||||
},
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_UnionsGalaxyRowsAndIncludes_RemovesExcludes_AndDeduplicates()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02" },
|
||||
// Duplicate of an include below (case-insensitive) — should appear once.
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Pump01.Fault", SourceObjectReference = "Pump01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
include: ["pump01.fault", "Valve03.Position.Lo"],
|
||||
exclude: ["Tank02.Level.HiHi"]));
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "Tank01.Level.HiHi", "Pump01.Fault", "Valve03.Position.Lo" },
|
||||
result.Select(t => t.ActiveSubtag.Replace(".active", string.Empty, StringComparison.Ordinal)));
|
||||
// De-dup preserved first (GR) occurrence; exclude removed Tank02.
|
||||
Assert.Equal(3, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ComposesSubtagAddressesFromConfigNames()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
subtags: new AlarmSubtagNameOptions
|
||||
{
|
||||
Active = "InAlarm",
|
||||
Acked = "Ack",
|
||||
Priority = "Pri",
|
||||
AckComment = "AckCmt",
|
||||
}));
|
||||
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Tank01.Level.HiHi.InAlarm", target.ActiveSubtag);
|
||||
Assert.Equal("Tank01.Level.HiHi.Ack", target.AckedSubtag);
|
||||
Assert.Equal("Tank01.Level.HiHi.Pri", target.PrioritySubtag);
|
||||
Assert.Equal("Tank01.Level.HiHi.AckCmt", target.AckCommentSubtag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_EmptyPriorityAndAckComment_LeaveThoseFieldsEmpty()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
// Default Priority is "priority"; force it empty alongside empty AckComment.
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
subtags: new AlarmSubtagNameOptions { Priority = string.Empty, AckComment = string.Empty }));
|
||||
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Tank01.Level.HiHi.active", target.ActiveSubtag);
|
||||
Assert.Equal("Tank01.Level.HiHi.acked", target.AckedSubtag);
|
||||
Assert.Equal(string.Empty, target.PrioritySubtag);
|
||||
Assert.Equal(string.Empty, target.AckCommentSubtag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ComposesCanonicalFullReference_WithArea()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(area: "Site_A"));
|
||||
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Galaxy!Site_A.Tank01.Level.HiHi", target.AlarmFullReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ComposesCanonicalFullReference_WithoutArea()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
// No discovery area and no default area.
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options());
|
||||
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_FallsBackToDefaultArea_WhenDiscoveryAreaEmpty()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(area: "", defaultArea: "Plant"));
|
||||
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Galaxy!Plant.Tank01.Level.HiHi", target.AlarmFullReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_UseGalaxyRepositoryFalse_DoesNotCallRepository_UsesIncludesOnly()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "ShouldNotAppear.X", SourceObjectReference = "ShouldNotAppear" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
useGalaxyRepository: false,
|
||||
include: ["Tank01.Level.HiHi"]));
|
||||
|
||||
Assert.Equal(0, repo.GetAlarmAttributesCount);
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_RepositoryThrows_LogsAndReturnsConfigOnlySet()
|
||||
{
|
||||
ThrowingGalaxyRepository repo = new(new InvalidOperationException("SQL unavailable"));
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
include: ["Tank01.Level.HiHi"]));
|
||||
|
||||
// Did not throw; discovery set empty, include retained.
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_DerivesSourceObjectForConfigEntry()
|
||||
{
|
||||
StubGalaxyRepository repo = new([]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
include: ["Tank01.Level.HiHi", "StandaloneTag"]));
|
||||
|
||||
Assert.Equal("Tank01", result[0].SourceObjectReference);
|
||||
// No dot — whole string is the source object.
|
||||
Assert.Equal("StandaloneTag", result[1].SourceObjectReference);
|
||||
}
|
||||
|
||||
/// <summary>In-memory <see cref="IGalaxyRepository"/> returning a fixed alarm rowset.</summary>
|
||||
private sealed class StubGalaxyRepository(List<GalaxyAlarmAttributeRow> rows) : IGalaxyRepository
|
||||
{
|
||||
/// <summary>Gets the number of times <see cref="GetAlarmAttributesAsync"/> was called.</summary>
|
||||
public int GetAlarmAttributesCount { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult<DateTime?>(null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(new List<GalaxyHierarchyRow>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(new List<GalaxyAttributeRow>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
||||
{
|
||||
GetAlarmAttributesCount++;
|
||||
return Task.FromResult(rows);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary><see cref="IGalaxyRepository"/> whose alarm-attribute query throws.</summary>
|
||||
private sealed class ThrowingGalaxyRepository(Exception toThrow) : IGalaxyRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult<DateTime?>(null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(new List<GalaxyHierarchyRow>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(new List<GalaxyAttributeRow>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default) =>
|
||||
Task.FromException<List<GalaxyAlarmAttributeRow>>(toThrow);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user