test: add E2E monitoring endpoint tests (varz, connz, healthz)

Replace Task.Delay polling with PeriodicTimer in NatsServerProcess readiness
checks and extend StartAsync to also TCP-poll the monitor port when enabled,
so MonitorServerFixture is guaranteed ready before tests run.
This commit is contained in:
Joseph Doherty
2026-03-12 19:10:33 -04:00
parent 4853409a40
commit 8ad2172e3c
3 changed files with 90 additions and 7 deletions

View File

@@ -100,6 +100,9 @@ public sealed class NatsServerProcess : IAsyncDisposable
_process.BeginErrorReadLine();
await WaitForTcpReadyAsync();
if (_enableMonitoring && MonitorPort.HasValue)
await WaitForMonitorPortReadyAsync();
}
public async ValueTask DisposeAsync()
@@ -115,9 +118,11 @@ public sealed class NatsServerProcess : IAsyncDisposable
{
await _process.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
catch (OperationCanceledException ex) when (!_process.HasExited)
{
// Already killed the tree above; nothing more to do
// Kill timed out and process is still running — force-terminate and surface the error
throw new InvalidOperationException(
$"NATS server process did not exit within 5s after kill.", ex);
}
}
@@ -136,8 +141,10 @@ public sealed class NatsServerProcess : IAsyncDisposable
private async Task WaitForTcpReadyAsync()
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100));
SocketException? lastError = null;
while (!timeout.Token.IsCancellationRequested)
while (await timer.WaitForNextTickAsync(timeout.Token).ConfigureAwait(false))
{
try
{
@@ -145,14 +152,38 @@ public sealed class NatsServerProcess : IAsyncDisposable
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, Port), timeout.Token);
return; // Connected — server is ready
}
catch (SocketException)
catch (SocketException ex)
{
await Task.Delay(100, timeout.Token);
lastError = ex; // Server not yet accepting connections — retry on next tick
}
}
throw new TimeoutException(
$"NATS server did not become ready on port {Port} within 10s.\n\nServer output:\n{Output}");
$"NATS server did not become ready on port {Port} within 10s. Last error: {lastError?.Message}\n\nServer output:\n{Output}");
}
private async Task WaitForMonitorPortReadyAsync()
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100));
SocketException? lastError = null;
while (await timer.WaitForNextTickAsync(timeout.Token).ConfigureAwait(false))
{
try
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, MonitorPort!.Value), timeout.Token);
return; // Monitor HTTP port is accepting connections
}
catch (SocketException ex)
{
lastError = ex; // Monitor not yet accepting connections — retry on next tick
}
}
throw new TimeoutException(
$"NATS monitor port {MonitorPort} did not become ready within 10s. Last error: {lastError?.Message}\n\nServer output:\n{Output}");
}
private static string ResolveHostDll()