Add XML documentation across gateway, worker, and .NET client

This commit is contained in:
Joseph Doherty
2026-04-30 11:49:58 -04:00
parent 4731ab535c
commit eed1e88a37
269 changed files with 4555 additions and 13 deletions
@@ -4,6 +4,7 @@ namespace MxGateway.IntegrationTests.Galaxy;
public sealed class GalaxyRepositoryLiveTests
{
/// <summary>Verifies that the Galaxy Repository can establish a live connection to the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task TestConnection_AgainstZb_Succeeds()
@@ -15,6 +16,7 @@ public sealed class GalaxyRepositoryLiveTests
Assert.True(ok, "TestConnectionAsync should return true against the ZB database.");
}
/// <summary>Verifies that the last deploy time can be retrieved from the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
@@ -26,6 +28,7 @@ public sealed class GalaxyRepositoryLiveTests
Assert.NotNull(lastDeploy);
}
/// <summary>Verifies that the hierarchy can be retrieved from the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetHierarchy_AgainstZb_ReturnsObjects()
@@ -43,6 +46,7 @@ public sealed class GalaxyRepositoryLiveTests
});
}
/// <summary>Verifies that object attributes can be retrieved from the ZB database.</summary>
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
@@ -1,10 +1,14 @@
namespace MxGateway.IntegrationTests.Galaxy;
/// <summary>Fact attribute that skips tests unless live Galaxy Repository tests are explicitly enabled.</summary>
public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
{
/// <summary>Environment variable name to enable live Galaxy Repository tests.</summary>
public const string EnableVariableName = "MXGATEWAY_RUN_LIVE_GALAXY_TESTS";
/// <summary>Environment variable name for the Galaxy Repository connection string.</summary>
public const string ConnectionStringVariableName = "MXGATEWAY_LIVE_GALAXY_CONN";
/// <summary>Initializes a new instance of the LiveGalaxyRepositoryFactAttribute class.</summary>
public LiveGalaxyRepositoryFactAttribute()
{
if (!Enabled)
@@ -13,12 +17,14 @@ public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
}
}
/// <summary>Gets a value indicating whether live Galaxy Repository tests are enabled.</summary>
public static bool Enabled =>
string.Equals(
Environment.GetEnvironmentVariable(EnableVariableName),
"1",
StringComparison.Ordinal);
/// <summary>Gets the Galaxy Repository connection string from environment or default.</summary>
public static string ConnectionString =>
Environment.GetEnvironmentVariable(ConnectionStringVariableName)
?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
@@ -8,27 +8,33 @@ public static class IntegrationTestEnvironment
public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";
/// <summary>Gets whether live MXAccess tests are enabled.</summary>
public static bool LiveMxAccessTestsEnabled =>
string.Equals(
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
"1",
StringComparison.Ordinal);
/// <summary>Gets the MXAccess item name for live tests.</summary>
public static string LiveMxAccessItem =>
GetOptionalEnvironmentVariable(
LiveMxAccessItemVariableName,
"TestChildObject.TestInt");
/// <summary>Gets the client name for live tests.</summary>
public static string LiveMxAccessClientName =>
GetOptionalEnvironmentVariable(
LiveMxAccessClientNameVariableName,
"MxGateway.IntegrationTests");
/// <summary>Gets the timeout for waiting on events in live tests.</summary>
public static TimeSpan LiveMxAccessEventTimeout =>
TimeSpan.FromSeconds(GetPositiveIntegerEnvironmentVariable(
LiveMxAccessEventTimeoutSecondsVariableName,
defaultValue: 15));
/// <summary>Resolves the path to the worker executable for live tests.</summary>
/// <returns>Path to MxGateway.Worker.exe.</returns>
public static string ResolveLiveMxAccessWorkerExecutablePath()
{
string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
@@ -74,6 +80,9 @@ public static class IntegrationTestEnvironment
return defaultValue;
}
/// <summary>Resolves the root directory of the repository by searching for .git and src directories.</summary>
/// <param name="startDirectory">Starting directory to search from.</param>
/// <returns>The repository root path, or the start directory if not found.</returns>
internal static string ResolveRepositoryRoot(string startDirectory)
{
DirectoryInfo? directory = new(startDirectory);
@@ -2,6 +2,7 @@ namespace MxGateway.IntegrationTests;
public sealed class IntegrationTestEnvironmentTests
{
/// <summary>Verifies that live MXAccess tests use correct environment variable name.</summary>
[Fact]
public void LiveMxAccessTests_AreOptInByEnvironmentVariable()
{
@@ -10,6 +11,7 @@ public sealed class IntegrationTestEnvironmentTests
IntegrationTestEnvironment.LiveMxAccessVariableName);
}
/// <summary>Verifies that worker executable uses correct environment variable name.</summary>
[Fact]
public void LiveMxAccessWorkerExecutable_UsesDocumentedEnvironmentVariable()
{
@@ -18,6 +20,7 @@ public sealed class IntegrationTestEnvironmentTests
IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName);
}
/// <summary>Verifies that repository root resolution accepts git worktree files.</summary>
[Fact]
public void ResolveRepositoryRoot_AcceptsGitWorktreeFile()
{
@@ -1,7 +1,9 @@
namespace MxGateway.IntegrationTests;
/// <summary>Marks an xUnit test as requiring installed MXAccess COM and live provider state.</summary>
public sealed class LiveMxAccessFactAttribute : FactAttribute
{
/// <summary>Initializes the attribute, skipping the test unless the integration test environment variable is set.</summary>
public LiveMxAccessFactAttribute()
{
if (!IntegrationTestEnvironment.LiveMxAccessTestsEnabled)
@@ -21,6 +21,9 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(15);
private static readonly TimeSpan StreamShutdownTimeout = TimeSpan.FromSeconds(10);
/// <summary>
/// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess.
/// </summary>
[LiveMxAccessFact]
[Trait("Category", "LiveMxAccess")]
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
@@ -208,12 +211,21 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
$"Event value_type={dataChange.Value?.DataType} raw_status={dataChange.RawStatus}");
}
/// <summary>
/// Test fixture that assembles the gateway service with a worker process factory for live MXAccess testing.
/// </summary>
private sealed class GatewayServiceFixture : IAsyncDisposable
{
private readonly GatewayMetrics _metrics = new();
private readonly SessionRegistry _registry = new();
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes the fixture with worker executable path, factory, and test output helper.
/// </summary>
/// <param name="workerExecutablePath">Path to the worker process executable.</param>
/// <param name="processFactory">Factory for creating worker processes.</param>
/// <param name="output">Test output helper for logging.</param>
public GatewayServiceFixture(
string workerExecutablePath,
IWorkerProcessFactory processFactory,
@@ -255,8 +267,14 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
_loggerFactory.CreateLogger<MxAccessGatewayService>());
}
/// <summary>
/// The assembled gateway service instance.
/// </summary>
public MxAccessGatewayService Service { get; }
/// <summary>
/// Disposes the fixture resources and closes all sessions.
/// </summary>
public async ValueTask DisposeAsync()
{
foreach (GatewaySession session in _registry.Snapshot())
@@ -295,12 +313,18 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
}
}
/// <summary>
/// Gathers messages written to a server stream for test inspection.
/// </summary>
private sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
{
private readonly object syncRoot = new();
private readonly TaskCompletionSource<T> firstMessage = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly List<T> messages = [];
/// <summary>
/// All messages that have been written to the stream.
/// </summary>
public IReadOnlyList<T> Messages
{
get
@@ -312,8 +336,15 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
}
}
/// <summary>
/// Inherited write options.
/// </summary>
public WriteOptions? WriteOptions { get; set; }
/// <summary>
/// Records the message and completes the first-message task.
/// </summary>
/// <param name="message">The message to write.</param>
public Task WriteAsync(T message)
{
lock (syncRoot)
@@ -325,12 +356,20 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
return Task.CompletedTask;
}
/// <summary>
/// Waits for the first message up to the specified timeout.
/// </summary>
/// <param name="timeout">The maximum time to wait.</param>
/// <returns>The first message written to the stream.</returns>
public async Task<T> WaitForFirstMessageAsync(TimeSpan timeout)
{
return await firstMessage.Task.WaitAsync(timeout).ConfigureAwait(false);
}
}
/// <summary>
/// Mock server call context for testing gRPC calls.
/// </summary>
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
{
private readonly Metadata requestHeaders = [];
@@ -339,43 +378,56 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
private Status status;
private WriteOptions? writeOptions;
/// <inheritdoc />
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
/// <inheritdoc />
protected override string HostCore => "localhost";
/// <inheritdoc />
protected override string PeerCore => "ipv4:127.0.0.1:5000";
/// <inheritdoc />
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
/// <inheritdoc />
protected override Metadata RequestHeadersCore => requestHeaders;
/// <inheritdoc />
protected override CancellationToken CancellationTokenCore => cancellationToken;
/// <inheritdoc />
protected override Metadata ResponseTrailersCore => responseTrailers;
/// <inheritdoc />
protected override Status StatusCore
{
get => status;
set => status = value;
}
/// <inheritdoc />
protected override WriteOptions? WriteOptionsCore
{
get => writeOptions;
set => writeOptions = value;
}
/// <inheritdoc />
protected override AuthContext AuthContextCore { get; } = new(
string.Empty,
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
/// <inheritdoc />
protected override IDictionary<object, object> UserStateCore => userState;
/// <inheritdoc />
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
{
return Task.CompletedTask;
}
/// <inheritdoc />
protected override ContextPropagationToken CreatePropagationTokenCore(
ContextPropagationOptions? options)
{
@@ -383,10 +435,14 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
}
}
/// <summary>
/// Factory that launches worker processes and records their outputs for testing.
/// </summary>
private sealed class TestWorkerProcessFactory(ITestOutputHelper output) : IWorkerProcessFactory
{
private readonly ConcurrentBag<TestWorkerProcess> processes = [];
/// <inheritdoc />
public IWorkerProcess Start(ProcessStartInfo startInfo)
{
startInfo.RedirectStandardError = true;
@@ -418,6 +474,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
return workerProcess;
}
/// <inheritdoc />
public async Task WaitForProcessesAsync(TimeSpan timeout)
{
foreach (TestWorkerProcess process in processes)
@@ -445,57 +502,77 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
}
}
/// <summary>
/// Adapter wrapping a System.Diagnostics.Process as IWorkerProcess for testing.
/// </summary>
private sealed class TestWorkerProcess(Process process) : IWorkerProcess
{
/// <inheritdoc />
public int Id => process.Id;
/// <inheritdoc />
public bool HasExited => process.HasExited;
/// <inheritdoc />
public int? ExitCode => process.HasExited ? process.ExitCode : null;
/// <inheritdoc />
public async ValueTask WaitForExitAsync(CancellationToken cancellationToken)
{
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public void Kill(bool entireProcessTree)
{
process.Kill(entireProcessTree);
}
/// <inheritdoc />
public void Dispose()
{
process.Dispose();
}
}
/// <summary>
/// Logger provider that writes all output to the test output helper.
/// </summary>
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
{
/// <inheritdoc />
public ILogger CreateLogger(string categoryName)
{
return new TestOutputLogger(output, categoryName);
}
/// <inheritdoc />
public void Dispose()
{
}
}
/// <summary>
/// Logger that writes messages to the test output helper.
/// </summary>
private sealed class TestOutputLogger(
ITestOutputHelper output,
string categoryName) : ILogger
{
/// <inheritdoc />
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull
{
return null;
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= LogLevel.Information;
}
/// <inheritdoc />
public void Log<TState>(
LogLevel logLevel,
EventId eventId,