diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs index f53ac77..b7bf9bc 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs @@ -32,7 +32,10 @@ public sealed class SqlServer : IAsyncLifetime .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("SQL Server is now ready for client connections")) .Build(); - private readonly ConcurrentDictionary> _databases = new(); + // Lazy, because GetOrAdd may run its factory more than once for callers that arrive together, and two of them + // would drop and create the same database under each other: with the project's rows run in parallel, the first + // rows of a sweep did, "Database 'typekit_queries' already exists" (#251). + private readonly ConcurrentDictionary>> _databases = new(); private string ConnectionString(string database) => new SqlConnectionStringBuilder { @@ -44,7 +47,7 @@ public sealed class SqlServer : IAsyncLifetime }.ConnectionString; /// A database of its own on the server, created empty the first time it is asked for, and its connection string. - public Task DatabaseAsync(string name) => _databases.GetOrAdd(name, CreateAsync); + public Task DatabaseAsync(string name) => _databases.GetOrAdd(name, key => new Lazy>(() => CreateAsync(key))).Value; private async Task CreateAsync(string name) { diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs new file mode 100644 index 0000000..63a5478 --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs @@ -0,0 +1,31 @@ +using Microsoft.Data.SqlClient; + +namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; + +/// +/// A guard on the harness: a database many callers ask for at once is created once, as the rows of a sweep ask for +/// it when they run in parallel (#251). Created more than once, one caller drops it under another, "Database +/// 'typekit_queries' already exists", or after another has returned, and every row reading it fails. +/// +[Collection(SqlServerCollection.Name)] +[Trait("Needs", "Docker")] +public sealed class SqlServerFixtureTests(SqlServer server) +{ + [Fact] + public async Task A_database_asked_for_by_many_callers_at_once_is_created_once() + { + var name = $"typekit_fixture_{Guid.NewGuid():N}"; + + var answers = await Task.WhenAll(Enumerable.Range(0, 32).Select(_ => Task.Run(() => server.DatabaseAsync(name)))); + + answers.Distinct().ShouldHaveSingleItem(); + + // A creation that lost the race could still drop the database after the winner returned; give it the time. + await Task.Delay(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken); + await using var connection = new SqlConnection(answers[0]); + await connection.OpenAsync(TestContext.Current.CancellationToken); + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT DB_NAME()"; + (await command.ExecuteScalarAsync(TestContext.Current.CancellationToken)).ShouldBe(name); + } +}