dc9c0c950c
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
504 lines
20 KiB
C#
504 lines
20 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
using ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Galaxy;
|
|
|
|
public sealed class GalaxyHierarchyCacheTests : IDisposable
|
|
{
|
|
private readonly List<string> _tempPaths = [];
|
|
|
|
/// <summary>
|
|
/// Verifies cache returns empty entry before any refresh occurs.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Current_BeforeAnyRefresh_ReturnsEmpty()
|
|
{
|
|
GalaxyDeployNotifier notifier = new();
|
|
ThrowingGalaxyRepository repository = new(new InvalidOperationException("not invoked"));
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider());
|
|
|
|
GalaxyHierarchyCacheEntry entry = cache.Current;
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Unknown, entry.Status);
|
|
Assert.False(entry.HasData);
|
|
Assert.Equal(0, entry.ObjectCount);
|
|
Assert.Empty(entry.Objects);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies cache marks unavailable and does not publish when the repository
|
|
/// surface throws — the production trigger for this code path is a SQL
|
|
/// connection failure, but it is fully covered by the cache's exception
|
|
/// branch and does not require a real TCP probe from a unit test.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenRepositoryThrows_MarksUnavailableAndDoesNotPublish()
|
|
{
|
|
GalaxyDeployNotifier notifier = new();
|
|
ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-04-28T12:00:00Z", System.Globalization.CultureInfo.InvariantCulture));
|
|
ThrowingGalaxyRepository repository = new(new InvalidOperationException("Galaxy repository unreachable"));
|
|
GalaxyHierarchyCache cache = new(repository, notifier, clock);
|
|
|
|
await cache.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Unavailable, cache.Current.Status);
|
|
Assert.Equal("Galaxy repository unreachable", cache.Current.LastError);
|
|
Assert.Null(notifier.Latest);
|
|
Assert.True(cache.WaitForFirstLoadAsync(CancellationToken.None).IsCompletedSuccessfully);
|
|
Assert.Equal(1, repository.GetLastDeployTimeCount);
|
|
Assert.Equal(0, repository.GetHierarchyCount);
|
|
Assert.Equal(0, repository.GetAttributesCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies HasData returns true for healthy cache entries.
|
|
/// </summary>
|
|
[Fact]
|
|
public void HasData_OnHealthyEntry_IsTrue()
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
|
|
{
|
|
Status = GalaxyCacheStatus.Healthy,
|
|
LastSuccessAt = DateTimeOffset.UtcNow,
|
|
ObjectCount = 1,
|
|
};
|
|
|
|
Assert.True(entry.HasData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies HasData returns false for unknown cache entries.
|
|
/// </summary>
|
|
[Fact]
|
|
public void HasData_OnUnknownEntry_IsFalse()
|
|
{
|
|
Assert.False(GalaxyHierarchyCacheEntry.Empty.HasData);
|
|
}
|
|
|
|
[Fact]
|
|
public void GalaxyHierarchyIndex_BuildsPathsAndTagLookupsWithoutThrowingOnBadMetadata()
|
|
{
|
|
GalaxyObject root = new()
|
|
{
|
|
GobjectId = 1,
|
|
TagName = "Area1",
|
|
ContainedName = "Area1",
|
|
};
|
|
GalaxyObject duplicate = new()
|
|
{
|
|
GobjectId = 1,
|
|
TagName = "DuplicateArea",
|
|
ContainedName = "DuplicateArea",
|
|
};
|
|
GalaxyObject child = new()
|
|
{
|
|
GobjectId = 2,
|
|
ParentGobjectId = 1,
|
|
TagName = "Pump_001",
|
|
ContainedName = "Pump",
|
|
Attributes =
|
|
{
|
|
new GalaxyAttribute
|
|
{
|
|
FullTagReference = "Pump_001.PV",
|
|
IsHistorized = true,
|
|
},
|
|
},
|
|
};
|
|
GalaxyObject orphan = new()
|
|
{
|
|
GobjectId = 3,
|
|
ParentGobjectId = 99,
|
|
TagName = "Orphan_001",
|
|
ContainedName = "Orphan",
|
|
};
|
|
|
|
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([root, duplicate, child, orphan]);
|
|
|
|
Assert.Equal("Area1/Pump", index.ObjectViewsById[2].ContainedPath);
|
|
Assert.Equal("Orphan", index.ObjectViewsById[3].ContainedPath);
|
|
Assert.Same(child, index.TagsByAddress["Pump_001.PV"].Object);
|
|
Assert.NotNull(index.TagsByAddress["Pump_001.PV"].Attribute);
|
|
Assert.Same(root, index.ObjectViewsById[1].Object);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies a successful refresh writes the browse dataset to the on-disk
|
|
/// snapshot store so a later cold start can restore it.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenSuccessful_PersistsSnapshotToDisk()
|
|
{
|
|
GalaxyDeployNotifier notifier = new();
|
|
StubGalaxyRepository repository = new(
|
|
deployTime: new DateTime(2026, 5, 20, 9, 0, 0, DateTimeKind.Utc),
|
|
hierarchy: [SampleHierarchyRow()],
|
|
attributes: [SampleAttributeRow()]);
|
|
GalaxyHierarchySnapshotStore store = CreateStore();
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), snapshotStore: store);
|
|
|
|
await cache.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Healthy, cache.Current.Status);
|
|
GalaxyHierarchySnapshot? persisted = await store.TryLoadAsync(CancellationToken.None);
|
|
Assert.NotNull(persisted);
|
|
Assert.Equal(99, Assert.Single(persisted.Hierarchy).GobjectId);
|
|
Assert.Equal("PV", Assert.Single(persisted.Attributes).AttributeName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that when the Galaxy database is unreachable on first refresh but a
|
|
/// snapshot exists on disk, the cache serves that data with <c>Stale</c> status
|
|
/// rather than coming up empty.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenDatabaseUnreachableButSnapshotOnDisk_RestoresStaleData()
|
|
{
|
|
GalaxyHierarchySnapshotStore store = CreateStore();
|
|
await store.SaveAsync(
|
|
new GalaxyHierarchySnapshot(
|
|
LastDeployTime: new DateTimeOffset(2026, 5, 20, 9, 0, 0, TimeSpan.Zero),
|
|
SavedAt: new DateTimeOffset(2026, 5, 20, 9, 1, 0, TimeSpan.Zero),
|
|
Hierarchy: [SampleHierarchyRow()],
|
|
Attributes: [SampleAttributeRow()]),
|
|
CancellationToken.None);
|
|
|
|
GalaxyDeployNotifier notifier = new();
|
|
ThrowingGalaxyRepository repository = new(new InvalidOperationException("Galaxy repository unreachable"));
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), snapshotStore: store);
|
|
|
|
await cache.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Stale, cache.Current.Status);
|
|
Assert.True(cache.Current.HasData);
|
|
Assert.Equal(1, cache.Current.ObjectCount);
|
|
Assert.Equal(1, cache.Current.AttributeCount);
|
|
Assert.NotNull(notifier.Latest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that when the disk snapshot's deploy time still matches the live
|
|
/// Galaxy database, the cache promotes the restored data to <c>Healthy</c>
|
|
/// without re-running the heavy hierarchy and attribute queries.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenSnapshotDeployMatchesLive_PromotesToHealthyWithoutHeavyQuery()
|
|
{
|
|
DateTime deployTime = new(2026, 5, 20, 9, 0, 0, DateTimeKind.Utc);
|
|
GalaxyHierarchySnapshotStore store = CreateStore();
|
|
await store.SaveAsync(
|
|
new GalaxyHierarchySnapshot(
|
|
LastDeployTime: new DateTimeOffset(deployTime, TimeSpan.Zero),
|
|
SavedAt: new DateTimeOffset(2026, 5, 20, 9, 1, 0, TimeSpan.Zero),
|
|
Hierarchy: [SampleHierarchyRow()],
|
|
Attributes: [SampleAttributeRow()]),
|
|
CancellationToken.None);
|
|
|
|
GalaxyDeployNotifier notifier = new();
|
|
StubGalaxyRepository repository = new(deployTime);
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), snapshotStore: store);
|
|
|
|
await cache.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Healthy, cache.Current.Status);
|
|
Assert.Equal(1, cache.Current.ObjectCount);
|
|
Assert.Equal(0, repository.GetHierarchyCount);
|
|
Assert.Equal(0, repository.GetAttributesCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a restored on-disk snapshot completes the first-load gate
|
|
/// immediately, so a browse call racing the first refresh is not blocked for
|
|
/// the full bootstrap budget while the live Galaxy query is still running.
|
|
/// Regression test for Server-033.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_RestoredSnapshotCompletesFirstLoadBeforeLiveQueryReturns()
|
|
{
|
|
GalaxyHierarchySnapshotStore store = CreateStore();
|
|
await store.SaveAsync(
|
|
new GalaxyHierarchySnapshot(
|
|
LastDeployTime: new DateTimeOffset(2026, 5, 20, 9, 0, 0, TimeSpan.Zero),
|
|
SavedAt: new DateTimeOffset(2026, 5, 20, 9, 1, 0, TimeSpan.Zero),
|
|
Hierarchy: [SampleHierarchyRow()],
|
|
Attributes: [SampleAttributeRow()]),
|
|
CancellationToken.None);
|
|
|
|
GalaxyDeployNotifier notifier = new();
|
|
BlockingGalaxyRepository repository = new();
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), snapshotStore: store);
|
|
|
|
Task refresh = cache.RefreshAsync(CancellationToken.None);
|
|
|
|
// The live query is blocked inside the repository; first-load must still
|
|
// complete — from the restored snapshot — well within the wait budget.
|
|
await cache.WaitForFirstLoadAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
|
|
Assert.True(cache.Current.HasData);
|
|
Assert.Equal(GalaxyCacheStatus.Stale, cache.Current.Status);
|
|
|
|
repository.Release();
|
|
await refresh.WaitAsync(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies a corrupt on-disk snapshot does not crash startup: the cache
|
|
/// ignores the unreadable file and comes up Unavailable when the database is
|
|
/// also unreachable. Regression test for Server-037.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenSnapshotFileCorrupt_ComesUpUnavailableWithoutThrowing()
|
|
{
|
|
string path = CreateTempPath();
|
|
await File.WriteAllTextAsync(path, "{ this is not valid json");
|
|
GalaxyHierarchySnapshotStore store = CreateStore(path);
|
|
|
|
GalaxyDeployNotifier notifier = new();
|
|
ThrowingGalaxyRepository repository = new(new InvalidOperationException("Galaxy repository unreachable"));
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), snapshotStore: store);
|
|
|
|
await cache.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Unavailable, cache.Current.Status);
|
|
Assert.False(cache.Current.HasData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that with snapshot persistence disabled the cache does not
|
|
/// restore from disk — an unreachable database leaves it Unavailable.
|
|
/// Regression test for Server-037.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenPersistDisabled_DoesNotRestoreFromDisk()
|
|
{
|
|
GalaxyHierarchySnapshotStore store = CreateStore(CreateTempPath(), persist: false);
|
|
|
|
GalaxyDeployNotifier notifier = new();
|
|
ThrowingGalaxyRepository repository = new(new InvalidOperationException("Galaxy repository unreachable"));
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), snapshotStore: store);
|
|
|
|
await cache.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(GalaxyCacheStatus.Unavailable, cache.Current.Status);
|
|
Assert.False(cache.Current.HasData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a snapshot save aborted because the gateway is shutting down
|
|
/// (the refresh token is cancelled) is not logged as a persistence failure.
|
|
/// Regression test for Server-036.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RefreshAsync_WhenSnapshotSaveCancelledAtShutdown_DoesNotLogPersistFailure()
|
|
{
|
|
using CancellationTokenSource cts = new();
|
|
GalaxyDeployNotifier notifier = new();
|
|
StubGalaxyRepository repository = new(
|
|
deployTime: new DateTime(2026, 5, 20, 9, 0, 0, DateTimeKind.Utc),
|
|
hierarchy: [SampleHierarchyRow()],
|
|
attributes: [SampleAttributeRow()]);
|
|
CancellingSaveStore store = new(cts);
|
|
RecordingLogger<GalaxyHierarchyCache> logger = new();
|
|
GalaxyHierarchyCache cache = new(repository, notifier, new ManualTimeProvider(), logger, store);
|
|
|
|
await cache.RefreshAsync(cts.Token);
|
|
|
|
Assert.DoesNotContain(
|
|
logger.Entries,
|
|
entry => entry.Level == LogLevel.Warning
|
|
&& entry.Message.Contains("persist", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static GalaxyHierarchyRow SampleHierarchyRow() => new()
|
|
{
|
|
GobjectId = 99,
|
|
TagName = "Pump_001",
|
|
ContainedName = "Pump",
|
|
BrowseName = "Pump",
|
|
CategoryId = 10,
|
|
TemplateChain = ["AppPump"],
|
|
};
|
|
|
|
private static GalaxyAttributeRow SampleAttributeRow() => new()
|
|
{
|
|
GobjectId = 99,
|
|
TagName = "Pump_001",
|
|
AttributeName = "PV",
|
|
FullTagReference = "Pump_001.PV",
|
|
MxDataType = 5,
|
|
DataTypeName = "Float",
|
|
};
|
|
|
|
private string CreateTempPath()
|
|
{
|
|
string path = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"mxgw-galaxy-cache-test-{Guid.NewGuid():N}.json");
|
|
_tempPaths.Add(path);
|
|
return path;
|
|
}
|
|
|
|
private GalaxyHierarchySnapshotStore CreateStore() => CreateStore(CreateTempPath());
|
|
|
|
private static GalaxyHierarchySnapshotStore CreateStore(string path, bool persist = true)
|
|
{
|
|
GalaxyRepositoryOptions options = new()
|
|
{
|
|
PersistSnapshot = persist,
|
|
SnapshotCachePath = path,
|
|
};
|
|
return new GalaxyHierarchySnapshotStore(Options.Create(options));
|
|
}
|
|
|
|
/// <summary><see cref="IGalaxyRepository"/> whose deploy-time query blocks until released.</summary>
|
|
private sealed class BlockingGalaxyRepository : IGalaxyRepository
|
|
{
|
|
private readonly TaskCompletionSource _release = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public void Release() => _release.TrySetResult();
|
|
|
|
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(false);
|
|
|
|
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
|
{
|
|
await _release.Task.WaitAsync(ct).ConfigureAwait(false);
|
|
throw new InvalidOperationException("Galaxy repository unreachable");
|
|
}
|
|
|
|
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
|
|
=> throw new InvalidOperationException("GetHierarchyAsync should not be reached");
|
|
|
|
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
|
|
=> throw new InvalidOperationException("GetAttributesAsync should not be reached");
|
|
}
|
|
|
|
/// <summary>Snapshot store whose <see cref="SaveAsync"/> cancels the token mid-save.</summary>
|
|
private sealed class CancellingSaveStore(CancellationTokenSource cts) : IGalaxyHierarchySnapshotStore
|
|
{
|
|
public Task<GalaxyHierarchySnapshot?> TryLoadAsync(CancellationToken cancellationToken)
|
|
=> Task.FromResult<GalaxyHierarchySnapshot?>(null);
|
|
|
|
public Task SaveAsync(GalaxyHierarchySnapshot snapshot, CancellationToken cancellationToken)
|
|
{
|
|
cts.Cancel();
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
/// <summary>Minimal <see cref="ILogger{T}"/> that records every emitted log entry.</summary>
|
|
private sealed class RecordingLogger<T> : ILogger<T>
|
|
{
|
|
public List<(LogLevel Level, string Message)> Entries { get; } = [];
|
|
|
|
public IDisposable BeginScope<TState>(TState state)
|
|
where TState : notnull => NullScope.Instance;
|
|
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
|
|
public void Log<TState>(
|
|
LogLevel logLevel,
|
|
EventId eventId,
|
|
TState state,
|
|
Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
{
|
|
Entries.Add((logLevel, formatter(state, exception)));
|
|
}
|
|
|
|
private sealed class NullScope : IDisposable
|
|
{
|
|
public static readonly NullScope Instance = new();
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>In-memory <see cref="IGalaxyRepository"/> that returns fixed rowsets.</summary>
|
|
private sealed class StubGalaxyRepository(
|
|
DateTime? deployTime,
|
|
List<GalaxyHierarchyRow>? hierarchy = null,
|
|
List<GalaxyAttributeRow>? attributes = null) : IGalaxyRepository
|
|
{
|
|
private readonly List<GalaxyHierarchyRow> _hierarchy = hierarchy ?? [];
|
|
private readonly List<GalaxyAttributeRow> _attributes = attributes ?? [];
|
|
|
|
public int GetHierarchyCount { get; private set; }
|
|
|
|
public int GetAttributesCount { get; private set; }
|
|
|
|
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) => Task.FromResult(deployTime);
|
|
|
|
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
|
|
{
|
|
GetHierarchyCount++;
|
|
return Task.FromResult(_hierarchy);
|
|
}
|
|
|
|
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
|
|
{
|
|
GetAttributesCount++;
|
|
return Task.FromResult(_attributes);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (string path in _tempPaths)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(path);
|
|
File.Delete(path + ".tmp");
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Best-effort cleanup of test scratch files.
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class ThrowingGalaxyRepository(Exception toThrow) : IGalaxyRepository
|
|
{
|
|
/// <summary>Gets the number of times <see cref="GetLastDeployTimeAsync"/> was called.</summary>
|
|
public int GetLastDeployTimeCount { get; private set; }
|
|
|
|
/// <summary>Gets the number of times <see cref="GetHierarchyAsync"/> was called.</summary>
|
|
public int GetHierarchyCount { get; private set; }
|
|
|
|
/// <summary>Gets the number of times <see cref="GetAttributesAsync"/> was called.</summary>
|
|
public int GetAttributesCount { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(false);
|
|
|
|
/// <inheritdoc />
|
|
public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
|
{
|
|
GetLastDeployTimeCount++;
|
|
throw toThrow;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
|
|
{
|
|
GetHierarchyCount++;
|
|
throw toThrow;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
|
|
{
|
|
GetAttributesCount++;
|
|
throw toThrow;
|
|
}
|
|
}
|
|
|
|
}
|