Skip to content

Commit 08004e2

Browse files
committed
fix(tests): update cancellation and reload assertions
1 parent 662aec9 commit 08004e2

2 files changed

Lines changed: 97 additions & 21 deletions

File tree

src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Net.Http.Json;
99
using System.Text.Json;
1010
using System.Threading.Tasks;
11+
using Azure.DataApiBuilder.Config;
1112
using Azure.DataApiBuilder.Config.ObjectModel;
1213
using Azure.DataApiBuilder.Core.Configurations;
1314
using Azure.DataApiBuilder.Service.Tests.SqlTests;
@@ -27,6 +28,7 @@ public class ConfigurationHotReloadTests
2728
private static RuntimeConfigProvider _configProvider;
2829
private static StringWriter _writer;
2930
private static readonly object _writerLock = new();
31+
private static HotReloadFailureObserver _hotReloadFailureObserver;
3032
private const string CONFIG_FILE_NAME = "hot-reload.dab-config.json";
3133
private const string GQL_QUERY_NAME = "books";
3234
private const string HOT_RELOAD_SUCCESS_MESSAGE = "Validated hot-reloaded configuration file";
@@ -229,6 +231,11 @@ public static async Task ClassInitializeAsync(TestContext context)
229231
{
230232
Console.WriteLine($"Initializing test server (attempt {attempt}/{maxRetries})...");
231233
_testServer = new(Program.CreateWebHostBuilder(new string[] { "--ConfigFileName", CONFIG_FILE_NAME }));
234+
_hotReloadFailureObserver = new(
235+
_testServer.Services.GetRequiredService<ILogger<FileSystemRuntimeConfigLoader>>());
236+
_testServer.Services
237+
.GetRequiredService<FileSystemRuntimeConfigLoader>()
238+
.SetLogger(_hotReloadFailureObserver);
232239
_testClient = _testServer.CreateClient();
233240
_configProvider = _testServer.Services.GetService<RuntimeConfigProvider>();
234241

@@ -316,6 +323,79 @@ private static bool WriterContains(string message)
316323
}
317324
}
318325

326+
/// <summary>
327+
/// Observes the loader's structured hot-reload failure log. The loader now owns and logs reload
328+
/// failures inside its serialized pipeline, so they no longer escape to ConfigFileWatcher's
329+
/// legacy Console.WriteLine fallback.
330+
/// </summary>
331+
private sealed class HotReloadFailureObserver(
332+
ILogger<FileSystemRuntimeConfigLoader> innerLogger) : ILogger<FileSystemRuntimeConfigLoader>
333+
{
334+
private readonly object _syncRoot = new();
335+
private TaskCompletionSource<string> _failureSource = CreateFailureSource();
336+
337+
public IDisposable? BeginScope<TState>(TState state)
338+
where TState : notnull => innerLogger.BeginScope(state);
339+
340+
public bool IsEnabled(LogLevel logLevel) =>
341+
logLevel == LogLevel.Error || innerLogger.IsEnabled(logLevel);
342+
343+
public void Log<TState>(
344+
LogLevel logLevel,
345+
EventId eventId,
346+
TState state,
347+
Exception? exception,
348+
Func<TState, Exception?, string> formatter)
349+
{
350+
innerLogger.Log(logLevel, eventId, state, exception, formatter);
351+
352+
if (logLevel != LogLevel.Error)
353+
{
354+
return;
355+
}
356+
357+
string message = formatter(state, exception);
358+
if (!message.Contains(
359+
HOT_RELOAD_FAILURE_MESSAGE,
360+
StringComparison.Ordinal))
361+
{
362+
return;
363+
}
364+
365+
RecordFailure(message);
366+
}
367+
368+
public void Reset()
369+
{
370+
lock (_syncRoot)
371+
{
372+
_failureSource = CreateFailureSource();
373+
}
374+
}
375+
376+
public async Task<string> WaitForFailureAsync(TimeSpan timeout)
377+
{
378+
Task<string> failureTask;
379+
lock (_syncRoot)
380+
{
381+
failureTask = _failureSource.Task;
382+
}
383+
384+
return await failureTask.WaitAsync(timeout);
385+
}
386+
387+
private static TaskCompletionSource<string> CreateFailureSource() =>
388+
new(TaskCreationOptions.RunContinuationsAsynchronously);
389+
390+
private void RecordFailure(string message)
391+
{
392+
lock (_syncRoot)
393+
{
394+
_failureSource.TrySetResult(message);
395+
}
396+
}
397+
}
398+
319399
/// <summary>
320400
/// Hot reload the configuration by saving a new file with different rest and graphQL paths.
321401
/// Validate that the response is correct when making a request with the newly hot-reloaded paths.
@@ -754,18 +834,15 @@ public async Task HotReloadConfigConnectionString()
754834

755835
// Act
756836
// Hot Reload should fail here
837+
_hotReloadFailureObserver.Reset();
757838
GenerateConfigFile(
758839
connectionString: $"WrongConnectionString");
759-
await WaitForConditionAsync(
760-
() => WriterContains(HOT_RELOAD_FAILURE_MESSAGE),
761-
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS),
762-
TimeSpan.FromMilliseconds(500));
840+
string failedConfigLog = await _hotReloadFailureObserver.WaitForFailureAsync(
841+
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS));
763842

764843
// Log that shows that hot-reload was not able to validate properly
765-
string failedConfigLog;
766844
lock (_writerLock)
767845
{
768-
failedConfigLog = _writer.ToString();
769846
_writer.GetStringBuilder().Clear();
770847
}
771848

@@ -851,19 +928,16 @@ public async Task HotReloadConfigDatabaseType()
851928

852929
// Act
853930
// Hot Reload should fail here
931+
_hotReloadFailureObserver.Reset();
854932
GenerateConfigFile(
855933
databaseType: DatabaseType.PostgreSQL,
856934
connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.POSTGRESQL).Replace("\\", "\\\\")}");
857-
await WaitForConditionAsync(
858-
() => WriterContains(HOT_RELOAD_FAILURE_MESSAGE),
859-
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS),
860-
TimeSpan.FromMilliseconds(500));
935+
string failedConfigLog = await _hotReloadFailureObserver.WaitForFailureAsync(
936+
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS));
861937

862938
// Log that shows that hot-reload was not able to validate properly
863-
string failedConfigLog;
864939
lock (_writerLock)
865940
{
866-
failedConfigLog = _writer.ToString();
867941
_writer.GetStringBuilder().Clear();
868942
}
869943

@@ -919,17 +993,16 @@ public async Task HotReloadValidationFail()
919993

920994
// Act
921995
// Generate a config that will fail validation by disabling REST, GraphQL, and MCP (which is not allowed)
996+
_hotReloadFailureObserver.Reset();
922997
GenerateConfigFile(
923998
connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.MSSQL).Replace("\\", "\\\\")}",
924999
restEnabled: "false",
9251000
gQLEnabled: "false",
9261001
mcpEnabled: "false");
9271002

9281003
// Wait for hot-reload to fail
929-
await WaitForConditionAsync(
930-
() => WriterContains(HOT_RELOAD_FAILURE_MESSAGE),
931-
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS),
932-
TimeSpan.FromMilliseconds(500));
1004+
await _hotReloadFailureObserver.WaitForFailureAsync(
1005+
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS));
9331006

9341007
RuntimeConfig newRuntimeConfig = _configProvider.GetConfig();
9351008

@@ -967,16 +1040,15 @@ public async Task HotReloadParsingFail()
9671040
bool originalGraphQLEnabled = lkgRuntimeConfig.Runtime.GraphQL.Enabled;
9681041

9691042
// Act
1043+
_hotReloadFailureObserver.Reset();
9701044
GenerateConfigFile(
9711045
connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.MSSQL).Replace("\\", "\\\\")}",
9721046
restEnabled: "invalid",
9731047
gQLEnabled: "invalid");
9741048

9751049
// Wait for hot-reload to fail (parsing error should trigger failure message)
976-
await WaitForConditionAsync(
977-
() => WriterContains(HOT_RELOAD_FAILURE_MESSAGE),
978-
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS),
979-
TimeSpan.FromMilliseconds(500));
1050+
await _hotReloadFailureObserver.WaitForFailureAsync(
1051+
TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS));
9801052

9811053
RuntimeConfig newRuntimeConfig = _configProvider.GetConfig();
9821054

src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.IO.Abstractions;
99
using System.Net;
1010
using System.Text.Json.Nodes;
11+
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Azure.DataApiBuilder.Config.DatabasePrimitives;
1314
using Azure.DataApiBuilder.Config.ObjectModel;
@@ -487,6 +488,7 @@ public async Task ValidateExceptionForInvalidResultFieldNames(string invalidFiel
487488
It.IsAny<IDictionary<string, DbConnectionParam>>(),
488489
It.IsAny<Func<DbDataReader, List<string>, Task<JsonArray>>>(),
489490
It.IsAny<string>(),
491+
It.IsAny<CancellationToken>(),
490492
It.IsAny<HttpContext>(),
491493
It.IsAny<List<string>>()))
492494
.ReturnsAsync(invalidFieldJsonArray);
@@ -514,7 +516,9 @@ public async Task ValidateExceptionForInvalidResultFieldNames(string invalidFiel
514516
{
515517
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
516518
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, ex.SubStatusCode);
517-
Assert.IsTrue(ex.Message.Contains("returns a column without a name"));
519+
Assert.IsTrue(
520+
ex.Message.Contains("returns a column without a name"),
521+
$"Unexpected validation exception: {ex.Message}");
518522
}
519523

520524
TestHelper.UnsetAllDABEnvironmentVariables();

0 commit comments

Comments
 (0)