diff --git a/tests/Aspire.Hosting.MySql.Tests/Aspire.Hosting.MySql.Tests.csproj b/tests/Aspire.Hosting.MySql.Tests/Aspire.Hosting.MySql.Tests.csproj index 3790d425768..04844332767 100644 --- a/tests/Aspire.Hosting.MySql.Tests/Aspire.Hosting.MySql.Tests.csproj +++ b/tests/Aspire.Hosting.MySql.Tests/Aspire.Hosting.MySql.Tests.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs index ea0165cc0e2..e489c761f7e 100644 --- a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs +++ b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs @@ -6,6 +6,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -27,7 +28,7 @@ public class MySqlFunctionalTests(ITestOutputHelper testOutputHelper) [RequiresDocker] public async Task VerifyWaitForOnMySqlBlocksDependentResources() { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + using var cts = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan); using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); var healthCheckTcs = new TaskCompletionSource(); @@ -58,14 +59,14 @@ public async Task VerifyWaitForOnMySqlBlocksDependentResources() await pendingStart; - await app.StopAsync(); + await app.StopAsync(cts.Token); } [Fact] [RequiresDocker] public async Task VerifyMySqlResource() { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var cts = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2); var pipeline = new ResiliencePipelineBuilder() .AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2), ShouldHandle = new PredicateBuilder().Handle() }) .Build(); @@ -79,9 +80,9 @@ public async Task VerifyMySqlResource() using var app = builder.Build(); - await app.StartAsync(); + await app.StartAsync(cts.Token); - await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2)); + await app.WaitForTextAsync(s_mySqlReadyText, cts.Token).WaitAsync(cts.Token); var hb = Host.CreateApplicationBuilder(); @@ -94,7 +95,7 @@ public async Task VerifyMySqlResource() using var host = hb.Build(); - await host.StartAsync(); + await host.StartAsync(cts.Token); await pipeline.ExecuteAsync(async token => { using var connection = host.Services.GetRequiredService(); @@ -119,7 +120,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) string? volumeName = null; string? bindMountPath = null; - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var cts = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2); var pipeline = new ResiliencePipelineBuilder() .AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2) }) .Build(); @@ -151,9 +152,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var app = builder1.Build()) { - await app.StartAsync(); + await app.StartAsync(cts.Token); - await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2)); + await app.WaitForTextAsync(s_mySqlReadyText, cts.Token).WaitAsync(cts.Token); try { @@ -168,7 +169,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var host = hb.Build()) { - await host.StartAsync(); + await host.StartAsync(cts.Token); // Wait until the database is available await pipeline.ExecuteAsync(async token => @@ -199,7 +200,7 @@ await pipeline.ExecuteAsync(async token => finally { // Stops the container, or the Volume/mount would still be in use - await app.StopAsync(); + await app.StopAsync(cts.Token); } } @@ -220,9 +221,9 @@ await pipeline.ExecuteAsync(async token => using (var app = builder2.Build()) { - await app.StartAsync(); + await app.StartAsync(cts.Token); - await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2)); + await app.WaitForTextAsync(s_mySqlReadyText, cts.Token).WaitAsync(cts.Token); try { @@ -230,14 +231,14 @@ await pipeline.ExecuteAsync(async token => hb.Configuration.AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(default) + [$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(cts.Token) }); hb.AddMySqlDataSource(db2.Resource.Name); using (var host = hb.Build()) { - await host.StartAsync(); + await host.StartAsync(cts.Token); // Wait until the database is available await pipeline.ExecuteAsync(async token => @@ -264,7 +265,7 @@ await pipeline.ExecuteAsync(async token => finally { // Stops the container, or the Volume/mount would still be in use - await app.StopAsync(); + await app.StopAsync(cts.Token); } } @@ -296,7 +297,7 @@ public async Task VerifyWithInitBindMount() { // Creates a script that should be executed when the container is initialized. - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var cts = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2); var pipeline = new ResiliencePipelineBuilder() .AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2), ShouldHandle = new PredicateBuilder().Handle() }) .Build(); @@ -323,22 +324,22 @@ public async Task VerifyWithInitBindMount() using var app = builder.Build(); - await app.StartAsync(); + await app.StartAsync(cts.Token); - await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2)); + await app.WaitForTextAsync(s_mySqlReadyText, cts.Token).WaitAsync(cts.Token); var hb = Host.CreateApplicationBuilder(); hb.Configuration.AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default) + [$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(cts.Token) }); hb.AddMySqlDataSource(db.Resource.Name); using var host = hb.Build(); - await host.StartAsync(); + await host.StartAsync(cts.Token); // Wait until the database is available await pipeline.ExecuteAsync(async token => @@ -380,7 +381,7 @@ await pipeline.ExecuteAsync(async token => [QuarantinedTest("https://github.com/dotnet/aspire/issues/7340")] public async Task VerifyEfMySql() { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var cts = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2); var pipeline = new ResiliencePipelineBuilder() .AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle() }) .Build(); @@ -394,22 +395,22 @@ public async Task VerifyEfMySql() using var app = builder.Build(); - await app.StartAsync(); + await app.StartAsync(cts.Token); - await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2)); + await app.WaitForTextAsync(s_mySqlReadyText, cts.Token).WaitAsync(cts.Token); var hb = Host.CreateApplicationBuilder(); hb.Configuration.AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default) + [$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(cts.Token) }); hb.AddMySqlDbContext(db.Resource.Name); using var host = hb.Build(); - await host.StartAsync(); + await host.StartAsync(cts.Token); // Wait until the database is available await pipeline.ExecuteAsync(async token => @@ -453,7 +454,7 @@ public async Task MySql_WithPersistentLifetime_ReusesContainers(bool useMultiple // it generates and mounts a config.user.inc.php file instead of using environment variables. // For this reason we need to test with and without multiple instances to cover both scenarios. - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + using var cts = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2); // Use the same path for both runs var aspireStorePath = Directory.CreateTempSubdirectory().FullName; @@ -511,7 +512,7 @@ public async Task MySql_WithPersistentLifetime_ReusesContainers(bool useMultiple resourceEvent = await rns.WaitForResourceHealthyAsync("resource-phpmyadmin", cts.Token); var phpMyAdminId = GetContainerId(resourceEvent); - await app.StopAsync(cts.Token).WaitAsync(TimeSpan.FromMinutes(1), cts.Token); + await app.StopAsync(cts.Token).WaitAsync(cts.Token); return [mySqlId, mySqlId2, phpMyAdminId]; } diff --git a/tests/Aspire.Hosting.Tests/Utils/LoggerNotificationExtensions.cs b/tests/Aspire.Hosting.Tests/Utils/LoggerNotificationExtensions.cs index af977ea4571..3bfe91b39ce 100644 --- a/tests/Aspire.Hosting.Tests/Utils/LoggerNotificationExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Utils/LoggerNotificationExtensions.cs @@ -28,7 +28,7 @@ public static Task WaitForTextAsync(this DistributedApplication app, string logT return WaitForTextAsync(app, (log) => log.Contains(logText), resourceName, cancellationToken); } - public static async Task WaitForHealthyAsync(this DistributedApplication app, IResourceBuilder resource, CancellationToken cancellationToken = default) where T: IResource + public static async Task WaitForHealthyAsync(this DistributedApplication app, IResourceBuilder resource, CancellationToken cancellationToken = default) where T : IResource { ArgumentNullException.ThrowIfNull(app); ArgumentNullException.ThrowIfNull(resource); @@ -52,6 +52,16 @@ public static Task WaitForTextAsync(this DistributedApplication app, IEnumerable return app.WaitForTextAsync((log) => logTexts.Any(x => log.Contains(x)), resourceName, cancellationToken); } + /// + /// Waits for the specified text to be logged. + /// + /// The instance to watch. + /// A predicate checking the text to wait for. + /// The cancellation token. + /// + public static Task WaitForTextAsync(this DistributedApplication app, Predicate predicate, CancellationToken cancellationToken = default) + => app.WaitForTextAsync(predicate, resourceName: null, cancellationToken); + /// /// Waits for the specified text to be logged. /// diff --git a/tests/Shared/AsyncTestHelpers.cs b/tests/Shared/AsyncTestHelpers.cs index c72cc383dd8..d3941eebd2b 100644 --- a/tests/Shared/AsyncTestHelpers.cs +++ b/tests/Shared/AsyncTestHelpers.cs @@ -20,11 +20,13 @@ internal static class TestConstants // Less time waiting for hang unit tests to fail in aspnetcore solution. public static readonly int DefaultTimeoutDuration = 5 * 1000 * (PlatformDetection.IsRunningOnCI ? 6 : 1); // 5 sec, 30 sec in CI public static readonly int LongTimeoutDuration = 60 * 1000 * (PlatformDetection.IsRunningOnCI ? 3 : 1); // 60 sec, 180 sec in CI + public static readonly int ExtraLongTimeoutDuration = 60 * 1000 * 3 * (PlatformDetection.IsRunningOnCI ? 2 : 1); // 180 sec, 360 sec in CI -- useful when a docker image might need pulling public static readonly int DefaultOrchestratorTestTimeout = 15 * 1000 * (PlatformDetection.IsRunningOnCI ? 2 : 1); // 15 sec, 30 sec in CI public static readonly int DefaultOrchestratorTestLongTimeout = 45 * 1000 * (PlatformDetection.IsRunningOnCI ? 4 : 1); // 45 sec, 180 sec in CI public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(DefaultTimeoutDuration); public static TimeSpan LongTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(LongTimeoutDuration); + public static TimeSpan ExtraLongTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(ExtraLongTimeoutDuration); } internal static class AsyncTestHelpers