Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
<ProjectReference Include="..\..\src\Components\Aspire.Pomelo.EntityFrameworkCore.MySql\Aspire.Pomelo.EntityFrameworkCore.MySql.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(TestsSharedDir)AsyncTestHelpers.cs" Link="shared/AsyncTestHelpers.cs" />
</ItemGroup>

</Project>
97 changes: 49 additions & 48 deletions tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,7 +28,7 @@ public class MySqlFunctionalTests(ITestOutputHelper testOutputHelper)
[RequiresDocker]
public async Task VerifyWaitForOnMySqlBlocksDependentResources()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var ct = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan).Token;
using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);

var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
Expand All @@ -44,28 +45,28 @@ public async Task VerifyWaitForOnMySqlBlocksDependentResources()

using var app = builder.Build();

var pendingStart = app.StartAsync(cts.Token);
var pendingStart = app.StartAsync(ct);

await app.ResourceNotifications.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);
await app.ResourceNotifications.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, ct);

await app.ResourceNotifications.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
await app.ResourceNotifications.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, ct);

healthCheckTcs.SetResult(HealthCheckResult.Healthy());

await app.ResourceNotifications.WaitForResourceHealthyAsync(resource.Resource.Name, cts.Token);
await app.ResourceNotifications.WaitForResourceHealthyAsync(resource.Resource.Name, ct);

await app.ResourceNotifications.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
await app.ResourceNotifications.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, ct);

await pendingStart;

await app.StopAsync();
await app.StopAsync(ct);
}

[Fact]
[RequiresDocker]
public async Task VerifyMySqlResource()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var ct = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2).Token;
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2), ShouldHandle = new PredicateBuilder().Handle<MySqlException>() })
.Build();
Expand All @@ -79,9 +80,9 @@ public async Task VerifyMySqlResource()

using var app = builder.Build();

await app.StartAsync();
await app.StartAsync(ct);

await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2));
await app.WaitForTextAsync(s_mySqlReadyText, ct).WaitAsync(ct);

var hb = Host.CreateApplicationBuilder();

Expand All @@ -94,7 +95,7 @@ public async Task VerifyMySqlResource()

using var host = hb.Build();

await host.StartAsync();
await host.StartAsync(ct);
await pipeline.ExecuteAsync(async token =>
{
using var connection = host.Services.GetRequiredService<MySqlConnection>();
Expand All @@ -105,7 +106,7 @@ await pipeline.ExecuteAsync(async token =>
var results = await command.ExecuteReaderAsync(token);

Assert.True(results.HasRows);
}, cts.Token);
}, ct);
}

[Theory]
Expand All @@ -119,7 +120,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
string? volumeName = null;
string? bindMountPath = null;

var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var ct = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2).Token;
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2) })
.Build();
Expand Down Expand Up @@ -151,9 +152,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)

using (var app = builder1.Build())
{
await app.StartAsync();
await app.StartAsync(ct);

await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2));
await app.WaitForTextAsync(s_mySqlReadyText, ct).WaitAsync(ct);

try
{
Expand All @@ -168,15 +169,15 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)

using (var host = hb.Build())
{
await host.StartAsync();
await host.StartAsync(ct);

// Wait until the database is available
await pipeline.ExecuteAsync(async token =>
{
using var connection = host.Services.GetRequiredService<MySqlConnection>();
await connection.OpenAsync(token);
Assert.Equal(ConnectionState.Open, connection.State);
}, cts.Token);
}, ct);

await pipeline.ExecuteAsync(async token =>
{
Expand All @@ -193,13 +194,13 @@ await pipeline.ExecuteAsync(async token =>
var results = await command.ExecuteReaderAsync(token);

Assert.True(results.HasRows);
}, cts.Token);
}, ct);
}
}
finally
{
// Stops the container, or the Volume/mount would still be in use
await app.StopAsync();
await app.StopAsync(ct);
}
}

Expand All @@ -220,32 +221,32 @@ await pipeline.ExecuteAsync(async token =>

using (var app = builder2.Build())
{
await app.StartAsync();
await app.StartAsync(ct);

await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2));
await app.WaitForTextAsync(s_mySqlReadyText, ct).WaitAsync(ct);

try
{
var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(default)
[$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(ct)
});

hb.AddMySqlDataSource(db2.Resource.Name);

using (var host = hb.Build())
{
await host.StartAsync();
await host.StartAsync(ct);

// Wait until the database is available
await pipeline.ExecuteAsync(async token =>
{
using var connection = host.Services.GetRequiredService<MySqlConnection>();
await connection.OpenAsync(token);
Assert.Equal(ConnectionState.Open, connection.State);
}, cts.Token);
}, ct);

await pipeline.ExecuteAsync(async token =>
{
Expand All @@ -257,14 +258,14 @@ await pipeline.ExecuteAsync(async token =>
var results = await command.ExecuteReaderAsync(token);

Assert.True(results.HasRows);
}, cts.Token);
}, ct);
}

}
finally
{
// Stops the container, or the Volume/mount would still be in use
await app.StopAsync();
await app.StopAsync(ct);
}
}

Expand Down Expand Up @@ -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));
var ct = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2).Token;
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2), ShouldHandle = new PredicateBuilder().Handle<MySqlException>() })
.Build();
Expand All @@ -323,30 +324,30 @@ public async Task VerifyWithInitBindMount()

using var app = builder.Build();

await app.StartAsync();
await app.StartAsync(ct);

await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2));
await app.WaitForTextAsync(s_mySqlReadyText, ct).WaitAsync(ct);

var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default)
[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(ct)
});

hb.AddMySqlDataSource(db.Resource.Name);

using var host = hb.Build();

await host.StartAsync();
await host.StartAsync(ct);

// Wait until the database is available
await pipeline.ExecuteAsync(async token =>
{
using var connection = host.Services.GetRequiredService<MySqlConnection>();
await connection.OpenAsync(token);
Assert.Equal(ConnectionState.Open, connection.State);
}, cts.Token);
}, ct);

await pipeline.ExecuteAsync(async token =>
{
Expand All @@ -360,7 +361,7 @@ await pipeline.ExecuteAsync(async token =>
Assert.True(await results.ReadAsync(token));
Assert.Equal("BatMobile", results.GetString("brand"));
Assert.False(await results.ReadAsync(token));
}, cts.Token);
}, ct);
}
finally
{
Expand All @@ -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));
var ct = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2).Token;
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle<MySqlException>() })
.Build();
Expand All @@ -394,53 +395,53 @@ public async Task VerifyEfMySql()

using var app = builder.Build();

await app.StartAsync();
await app.StartAsync(ct);

await app.WaitForTextAsync(s_mySqlReadyText).WaitAsync(TimeSpan.FromMinutes(2));
await app.WaitForTextAsync(s_mySqlReadyText, ct).WaitAsync(ct);

var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default)
[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(ct)
});

hb.AddMySqlDbContext<TestDbContext>(db.Resource.Name);

using var host = hb.Build();

await host.StartAsync();
await host.StartAsync(ct);

// Wait until the database is available
await pipeline.ExecuteAsync(async token =>
{
var dbContext = host.Services.GetRequiredService<TestDbContext>();
var databaseCreator = (RelationalDatabaseCreator)dbContext.Database.GetService<IDatabaseCreator>();
Assert.True(await databaseCreator.CanConnectAsync(token));
}, cts.Token);
}, ct);

// Initialize database schema
await pipeline.ExecuteAsync(async token =>
{
var dbContext = host.Services.GetRequiredService<TestDbContext>();
var databaseCreator = (RelationalDatabaseCreator)dbContext.Database.GetService<IDatabaseCreator>();
await databaseCreator.CreateTablesAsync(token);
}, cts.Token);
}, ct);

await pipeline.ExecuteAsync(async token =>
{
var dbContext = host.Services.GetRequiredService<TestDbContext>();
dbContext.Cars.Add(new TestDbContext.Car { Brand = "BatMobile" });
await dbContext.SaveChangesAsync(token);
}, cts.Token);
}, ct);

await pipeline.ExecuteAsync(async token =>
{
var dbContext = host.Services.GetRequiredService<TestDbContext>();
var cars = await dbContext.Cars.ToListAsync(token);
Assert.Single(cars);
Assert.Equal("BatMobile", cars[0].Brand);
}, cts.Token);
}, ct);
}

[Theory]
Expand All @@ -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));
var ct = new CancellationTokenSource(TestConstants.ExtraLongTimeoutTimeSpan * 2).Token;

// Use the same path for both runs
var aspireStorePath = Directory.CreateTempSubdirectory().FullName;
Expand Down Expand Up @@ -493,25 +494,25 @@ public async Task MySql_WithPersistentLifetime_ReusesContainers(bool useMultiple
}

var app = builder.Build();
await app.StartAsync(cts.Token);
await app.StartAsync(ct);

var rns = app.Services.GetRequiredService<ResourceNotificationService>();

var resourceEvent = await rns.WaitForResourceHealthyAsync("resource", cts.Token);
var resourceEvent = await rns.WaitForResourceHealthyAsync("resource", ct);
var mySqlId = GetContainerId(resourceEvent);

var mySqlId2 = "";

if (useMultipleInstances)
{
resourceEvent = await rns.WaitForResourceHealthyAsync("resource2", cts.Token);
resourceEvent = await rns.WaitForResourceHealthyAsync("resource2", ct);
mySqlId2 = GetContainerId(resourceEvent);
}

resourceEvent = await rns.WaitForResourceHealthyAsync("resource-phpmyadmin", cts.Token);
resourceEvent = await rns.WaitForResourceHealthyAsync("resource-phpmyadmin", ct);
var phpMyAdminId = GetContainerId(resourceEvent);

await app.StopAsync(cts.Token).WaitAsync(TimeSpan.FromMinutes(1), cts.Token);
await app.StopAsync(ct).WaitAsync(ct);

return [mySqlId, mySqlId2, phpMyAdminId];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(this DistributedApplication app, IResourceBuilder<T> resource, CancellationToken cancellationToken = default) where T: IResource
public static async Task WaitForHealthyAsync<T>(this DistributedApplication app, IResourceBuilder<T> resource, CancellationToken cancellationToken = default) where T : IResource
{
ArgumentNullException.ThrowIfNull(app);
ArgumentNullException.ThrowIfNull(resource);
Expand All @@ -52,6 +52,16 @@ public static Task WaitForTextAsync(this DistributedApplication app, IEnumerable
return app.WaitForTextAsync((log) => logTexts.Any(x => log.Contains(x)), resourceName, cancellationToken);
}

/// <summary>
/// Waits for the specified text to be logged.
/// </summary>
/// <param name="app">The <see cref="DistributedApplication" /> instance to watch.</param>
/// <param name="predicate">A predicate checking the text to wait for.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
public static Task WaitForTextAsync(this DistributedApplication app, Predicate<string> predicate, CancellationToken cancellationToken = default)
=> app.WaitForTextAsync(predicate, resourceName: null, cancellationToken);

/// <summary>
/// Waits for the specified text to be logged.
/// </summary>
Expand Down
Loading
Loading