f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
561 lines
27 KiB
C#
561 lines
27 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.GalaxyRepository;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
|
|
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);
|
|
|
|
/// <summary>
|
|
/// Resolves and projects to the target list. These cases assert composition rules, which
|
|
/// live in <see cref="AlarmWatchListResolution.Targets"/>; the discovery-provenance fields
|
|
/// the resolution also carries are asserted by their own cases below.
|
|
/// </summary>
|
|
/// <param name="resolver">The resolver under test.</param>
|
|
/// <param name="options">Alarm options to resolve against.</param>
|
|
/// <param name="cancellationToken">Token to cancel the resolution.</param>
|
|
/// <returns>The resolved targets.</returns>
|
|
private static async Task<IReadOnlyList<AlarmSubtagTarget>> ResolveTargetsAsync(
|
|
AlarmWatchListResolver resolver,
|
|
AlarmsOptions options,
|
|
CancellationToken cancellationToken = default) =>
|
|
(await resolver.ResolveAsync(options, cancellationToken)).Targets;
|
|
|
|
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(),
|
|
},
|
|
};
|
|
|
|
/// <summary>
|
|
/// Verifies Galaxy Repository rows and config includes are unioned, configured
|
|
/// excludes are removed, and duplicate references (case-insensitive) collapse
|
|
/// to a single entry.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_UnionsGalaxyRowsAndIncludes_RemovesExcludes_AndDeduplicates()
|
|
{
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02", Area = "TestArea" },
|
|
// Duplicate of an include below (case-insensitive) — should appear once.
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Pump01.Fault", SourceObjectReference = "Pump01", Area = "TestArea" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, 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(".InAlarm", string.Empty, StringComparison.Ordinal)));
|
|
// De-dup preserved first (GR) occurrence; exclude removed Tank02.
|
|
Assert.Equal(3, result.Count);
|
|
}
|
|
|
|
/// <summary>Verifies subtag item addresses are composed using the configured subtag names.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ComposesSubtagAddressesFromConfigNames()
|
|
{
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, 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);
|
|
}
|
|
|
|
/// <summary>Verifies that configuring empty Priority and AckComment subtag names leaves those target fields empty.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_EmptyPriorityAndAckComment_LeaveThoseFieldsEmpty()
|
|
{
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
// Default Priority is "Priority"; force it empty alongside empty AckComment.
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
subtags: new AlarmSubtagNameOptions { Priority = string.Empty, AckComment = string.Empty }));
|
|
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Equal("Tank01.Level.HiHi.InAlarm", target.ActiveSubtag);
|
|
Assert.Equal("Tank01.Level.HiHi.Acked", target.AckedSubtag);
|
|
Assert.Equal(string.Empty, target.PrioritySubtag);
|
|
Assert.Equal(string.Empty, target.AckCommentSubtag);
|
|
}
|
|
|
|
/// <summary>Verifies the canonical full reference uses the row's discovered Galaxy area, not the configured Discovery.Area.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ComposesCanonicalFullReference_FromRealGalaxyArea_NotConfigArea()
|
|
{
|
|
// The GR row carries the object's real Galaxy area (the alarmmgr group). The
|
|
// composed reference must use that area, NOT the configured Discovery.Area.
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow
|
|
{
|
|
FullTagReference = "TestMachine_001.TestAlarm001",
|
|
SourceObjectReference = "TestMachine_001",
|
|
Area = "TestArea",
|
|
},
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
// Config area "DEV" must be ignored for a GR row that has a discovered area.
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver,
|
|
Options(area: "DEV", defaultArea: "DEV"));
|
|
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Equal("Galaxy!TestArea.TestMachine_001.TestAlarm001", target.AlarmFullReference);
|
|
}
|
|
|
|
/// <summary>Verifies the canonical full reference omits the area segment when neither a discovered nor a configured area is present.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ComposesCanonicalFullReference_WithoutArea()
|
|
{
|
|
// GR row with no discovered area and no config area -> bare Galaxy!{reference}.
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
// No discovery area and no default area.
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options());
|
|
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
|
|
}
|
|
|
|
/// <summary>Verifies a config include entry with no discovered area falls back to the configured Discovery.Area.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ConfigInclude_UsesDiscoveryAreaFallback()
|
|
{
|
|
// A config IncludeAttributes entry has no discovered area, so it uses the
|
|
// config fallback: Discovery.Area when set.
|
|
StubGalaxyRepository repo = new([]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
area: "Site_A",
|
|
include: ["Tank01.Level.HiHi"]));
|
|
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Equal("Galaxy!Site_A.Tank01.Level.HiHi", target.AlarmFullReference);
|
|
}
|
|
|
|
/// <summary>Verifies a config include entry falls back to DefaultArea when Discovery.Area is empty.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ConfigInclude_FallsBackToDefaultArea_WhenDiscoveryAreaEmpty()
|
|
{
|
|
// A config IncludeAttributes entry with no Discovery.Area uses DefaultArea.
|
|
StubGalaxyRepository repo = new([]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
area: "",
|
|
defaultArea: "Plant",
|
|
include: ["Tank01.Level.HiHi"]));
|
|
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Equal("Galaxy!Plant.Tank01.Level.HiHi", target.AlarmFullReference);
|
|
}
|
|
|
|
/// <summary>Verifies that disabling Galaxy Repository discovery skips the repository call and resolves the watch list from configured includes only.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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 ResolveTargetsAsync(resolver, 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);
|
|
}
|
|
|
|
/// <summary>Verifies that a repository exception during discovery is logged and swallowed, returning the config-only watch list instead of throwing.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_RepositoryThrows_LogsAndReturnsConfigOnlySet()
|
|
{
|
|
ThrowingGalaxyRepository repo = new(new InvalidOperationException("SQL unavailable"));
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swallowing the discovery failure is right for the resolver but leaves the
|
|
/// caller unable to tell "discovery produced nothing" from "nothing was configured", which
|
|
/// is why the empty watch-list could sit unnoticed for a monitor's whole life. The
|
|
/// resolution therefore reports whether discovery was attempted, whether it failed, and
|
|
/// the failure message the provider status quotes.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_RepositoryThrows_ReportsDiscoveryFailureOnTheResolution()
|
|
{
|
|
ThrowingGalaxyRepository repo = new(new InvalidOperationException("SQL unavailable"));
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
AlarmWatchListResolution resolution = await resolver.ResolveAsync(Options());
|
|
|
|
Assert.Empty(resolution.Targets);
|
|
Assert.True(resolution.DiscoveryAttempted);
|
|
Assert.True(resolution.DiscoveryFailed);
|
|
Assert.Equal("SQL unavailable", resolution.DiscoveryFailureMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The complement of the case above: discovery disabled is not discovery failed. An empty
|
|
/// watch-list here means the deployment did not ask for subtag coverage, which the monitor
|
|
/// must not report as a degraded standby.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_RepositoryDisabled_ReportsDiscoveryNotAttempted()
|
|
{
|
|
StubGalaxyRepository repo = new([]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
AlarmWatchListResolution resolution = await resolver.ResolveAsync(
|
|
Options(useGalaxyRepository: false));
|
|
|
|
Assert.Empty(resolution.Targets);
|
|
Assert.False(resolution.DiscoveryAttempted);
|
|
Assert.False(resolution.DiscoveryFailed);
|
|
Assert.Null(resolution.DiscoveryFailureMessage);
|
|
}
|
|
|
|
/// <summary>Verifies the source object reference is derived from a config include entry, using the portion before the first dot when present.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_DerivesSourceObjectForConfigEntry()
|
|
{
|
|
StubGalaxyRepository repo = new([]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, 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>
|
|
/// Fix 1: ExcludeAttributes must be ignored when UseGalaxyRepository is false.
|
|
/// A config-only include must survive even when the same path appears in ExcludeAttributes.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ExcludeIgnored_WhenGalaxyRepositoryDisabled()
|
|
{
|
|
// Repo is never consulted; only IncludeAttributes matters.
|
|
StubGalaxyRepository repo = new([]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
useGalaxyRepository: false,
|
|
include: ["Tank01.Level.HiHi"],
|
|
exclude: ["Tank01.Level.HiHi"]));
|
|
|
|
// ExcludeAttributes is ignored when GR is off — the include must be present.
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fix 1 (GR-on path): ExcludeAttributes still prunes GR rows when
|
|
/// UseGalaxyRepository is true.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ExcludeApplied_WhenGalaxyRepositoryEnabled()
|
|
{
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02", Area = "TestArea" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
useGalaxyRepository: true,
|
|
exclude: ["Tank02.Level.HiHi"]));
|
|
|
|
// Tank02 was excluded; only Tank01 remains.
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fix 1: A whitespace-only ExcludeAttributes entry must be skipped and must
|
|
/// not accidentally exclude any real reference.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_WhitespaceOnlyExcludeEntry_IsSkipped()
|
|
{
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
// The exclude array contains a whitespace-only string — should be a no-op.
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
useGalaxyRepository: true,
|
|
exclude: [" "]));
|
|
|
|
// Tank01 must not have been wrongly excluded.
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A cancellation triggered while Galaxy Repository discovery is
|
|
/// awaiting must propagate as <see cref="OperationCanceledException"/>, not be
|
|
/// swallowed into a config-only watch-list, per the <see cref="IAlarmWatchListResolver"/>
|
|
/// contract.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_RepositoryCancelled_PropagatesOperationCanceled()
|
|
{
|
|
using CancellationTokenSource cts = new();
|
|
// Repository observes the token, cancels the source, then throws the matching
|
|
// OperationCanceledException — exactly what the live SQL path does on shutdown.
|
|
CancellingGalaxyRepository repo = new(cts);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
|
ResolveTargetsAsync(resolver,
|
|
Options(include: ["Tank01.Level.HiHi"]),
|
|
cts.Token));
|
|
}
|
|
|
|
/// <summary>
|
|
/// An entry that appears in both
|
|
/// <c>IncludeAttributes</c> and <c>ExcludeAttributes</c> is removed — excludes
|
|
/// win over explicit includes (the documented "excludes also suppress matching
|
|
/// explicit includes" behaviour).
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResolveAsync_ExcludeAlsoSuppressesMatchingExplicitInclude()
|
|
{
|
|
StubGalaxyRepository repo = new(
|
|
[
|
|
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", Area = "TestArea" },
|
|
]);
|
|
|
|
AlarmWatchListResolver resolver = CreateResolver(repo);
|
|
|
|
// Pump01.Fault is an explicit include AND an exclude (case-insensitive). It must
|
|
// be dropped; the GR row Tank01 survives.
|
|
IReadOnlyList<AlarmSubtagTarget> result = await ResolveTargetsAsync(resolver, Options(
|
|
useGalaxyRepository: true,
|
|
include: ["Pump01.Fault"],
|
|
exclude: ["pump01.fault"]));
|
|
|
|
AlarmSubtagTarget target = Assert.Single(result);
|
|
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(result, t => t.ActiveSubtag.Contains("Pump01", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
/// <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; }
|
|
|
|
/// <summary>Always reports a successful connection.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying <see langword="true"/>.</returns>
|
|
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
/// <summary>Reports no known deploy time.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying <see langword="null"/>.</returns>
|
|
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
|
|
Task.FromResult<DateTime?>(null);
|
|
|
|
/// <summary>Not exercised by these tests; returns an empty hierarchy.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying an empty list.</returns>
|
|
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
|
|
Task.FromResult(new List<GalaxyHierarchyRow>());
|
|
|
|
/// <summary>Not exercised by these tests; returns an empty attribute set.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying an empty list.</returns>
|
|
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
|
|
Task.FromResult(new List<GalaxyAttributeRow>());
|
|
|
|
/// <summary>Returns the fixed alarm rowset supplied at construction, tracking how many times it was called.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying the configured alarm attribute rows.</returns>
|
|
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
|
|
{
|
|
/// <summary>Always reports a successful connection; only alarm-attribute discovery fails for this fake.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying <see langword="true"/>.</returns>
|
|
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
/// <summary>Reports no known deploy time.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying <see langword="null"/>.</returns>
|
|
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
|
|
Task.FromResult<DateTime?>(null);
|
|
|
|
/// <summary>Not exercised by these tests; returns an empty hierarchy.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying an empty list.</returns>
|
|
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
|
|
Task.FromResult(new List<GalaxyHierarchyRow>());
|
|
|
|
/// <summary>Not exercised by these tests; returns an empty attribute set.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying an empty list.</returns>
|
|
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
|
|
Task.FromResult(new List<GalaxyAttributeRow>());
|
|
|
|
/// <summary>Simulates the live SQL path failing by faulting with the configured exception.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A faulted task carrying the configured exception.</returns>
|
|
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default) =>
|
|
Task.FromException<List<GalaxyAlarmAttributeRow>>(toThrow);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="IGalaxyRepository"/> whose alarm-attribute query cancels the
|
|
/// supplied source and throws a token-bound <see cref="OperationCanceledException"/>,
|
|
/// mirroring the live SQL path being cancelled mid-await.
|
|
/// </summary>
|
|
private sealed class CancellingGalaxyRepository(CancellationTokenSource source) : IGalaxyRepository
|
|
{
|
|
/// <summary>Always reports a successful connection; only alarm-attribute discovery cancels for this fake.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying <see langword="true"/>.</returns>
|
|
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
/// <summary>Reports no known deploy time.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying <see langword="null"/>.</returns>
|
|
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
|
|
Task.FromResult<DateTime?>(null);
|
|
|
|
/// <summary>Not exercised by these tests; returns an empty hierarchy.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying an empty list.</returns>
|
|
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
|
|
Task.FromResult(new List<GalaxyHierarchyRow>());
|
|
|
|
/// <summary>Not exercised by these tests; returns an empty attribute set.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>A completed task carrying an empty list.</returns>
|
|
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
|
|
Task.FromResult(new List<GalaxyAttributeRow>());
|
|
|
|
/// <summary>Cancels the supplied source and throws, mirroring the live SQL path being cancelled mid-await.</summary>
|
|
/// <param name="ct">Token to observe for cancellation.</param>
|
|
/// <returns>Never returns normally; always throws <see cref="OperationCanceledException"/>.</returns>
|
|
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
|
{
|
|
source.Cancel();
|
|
ct.ThrowIfCancellationRequested();
|
|
return Task.FromResult(new List<GalaxyAlarmAttributeRow>());
|
|
}
|
|
}
|
|
}
|