From 0df62c1109f3b8bfed29b42c76f242143c633658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Sat, 31 Oct 2026 11:09:23 +0100 Subject: [PATCH 1/2] test: run the EF Core query sweeps' rows in parallel, and create each SQL Server database once After #250, CI's Linux leg ends every run, and the EF Core project is half of it: its two query sweeps are one class each of 12,275 rows, which xunit's default mode ran one after another (#251). A row waits on SQLite, SQL Server or PostgreSQL more than it computes: at four threads the project took 55 s instead of 123 s for the same CPU. The project now runs with parallelMode "all" (xunit.runner.json). A row reads only what was seeded for its model, and each SQLite class opens its own database per test. SqlServerRoundTripTests opts out ([TestClass(DisableParallelization = true)]): each of its tests creates the same tables in one database. Seeded by removing the opt-out: 5 of its 7 tests fail. The whole suite found a race in the SQL Server fixture: DatabaseAsync's GetOrAdd ran its factory more than once for callers that arrived together, and the first rows of a sweep dropped and created typekit_queries under each other. Each database is now created once behind a Lazy, as the sweeps' own seeded databases are. SqlServerFixtureTests asks for one database from 32 callers at once; with the plain GetOrAdd restored it fails 3 runs of 3. The profile also answered the issue's other half: a CMTK0003 sweep row costs its own file, and reusing the compilation or compiling the declarations to metadata saves nothing, so DroppedTaskVariantTests is unchanged. Co-Authored-By: Claude Opus 5.5 --- ...s.TypeKit.EntityFrameworkCore.Tests.csproj | 13 ++++++++ .../SqlServer.cs | 7 +++-- .../SqlServerFixtureTests.cs | 31 +++++++++++++++++++ .../SqlServerRoundTripTests.cs | 5 +++ .../xunit.runner.json | 4 +++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs create mode 100644 test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj index ce8a11d..cd08757 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj @@ -24,4 +24,17 @@ + + + + + diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs index f53ac77..899c152 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: in the project's parallel mode 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..55da554 --- /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 rows ask for at once is created once, as the project's parallel mode +/// asks for it (#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); + } +} diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs index 52cd744..802cb65 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs @@ -12,7 +12,12 @@ namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; /// and what SQL Server does differently from memory (design/knowledge KB-048 to KB-054) a value object does as the /// type it wraps does. /// +/// +/// Its tests run one at a time: each creates the same tables in the one database typekit_round_trips, which +/// the project's parallel mode would race ("There is already an object named 'Customers'", #251). +/// [Collection(SqlServerCollection.Name)] +[TestClass(DisableParallelization = true)] [Trait("Needs", "Docker")] public sealed class SqlServerRoundTripTests(SqlServer server) { diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json new file mode 100644 index 0000000..82eb3ce --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/v3/xunit.runner.schema.json", + "parallelMode": "all" +} From f25a8d1241b1d2c4ea6330eb2b24f17111a28fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Sat, 31 Oct 2026 11:46:11 +0100 Subject: [PATCH 2/2] test: run the EF Core rows one collection at a time again, keeping the fixture's create-once On CI, parallel rows bought the EF Core project almost nothing (#251, #252's run): the database containers share the runner's four cores with the tests, so what looked like waiting on a local machine with spare cores became CPU. The Linux test step went from 9m 28s to 9m 17s, and the Windows one from 5m 17s to 7m 11s, as the projects beside EF Core slowed. xunit.runner.json and SqlServerRoundTripTests' opt-out go. SqlServer.DatabaseAsync keeps creating each database once behind a Lazy, with SqlServerFixtureTests as its guard: GetOrAdd may run its factory twice for callers that arrive together, which rows run in parallel did. Co-Authored-By: Claude Opus 5.5 --- ...doMetis.TypeKit.EntityFrameworkCore.Tests.csproj | 13 ------------- .../SqlServer.cs | 4 ++-- .../SqlServerFixtureTests.cs | 6 +++--- .../SqlServerRoundTripTests.cs | 5 ----- .../xunit.runner.json | 4 ---- 5 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj index cd08757..ce8a11d 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj @@ -24,17 +24,4 @@ - - - - - diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs index 899c152..b7bf9bc 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs @@ -33,8 +33,8 @@ public sealed class SqlServer : IAsyncLifetime .Build(); // 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: in the project's parallel mode the first rows of a - // sweep did, "Database 'typekit_queries' already exists" (#251). + // 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 diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs index 55da554..63a5478 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerFixtureTests.cs @@ -3,9 +3,9 @@ namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; /// -/// A guard on the harness: a database many rows ask for at once is created once, as the project's parallel mode -/// asks for it (#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. +/// 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")] diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs index 802cb65..52cd744 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs @@ -12,12 +12,7 @@ namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; /// and what SQL Server does differently from memory (design/knowledge KB-048 to KB-054) a value object does as the /// type it wraps does. /// -/// -/// Its tests run one at a time: each creates the same tables in the one database typekit_round_trips, which -/// the project's parallel mode would race ("There is already an object named 'Customers'", #251). -/// [Collection(SqlServerCollection.Name)] -[TestClass(DisableParallelization = true)] [Trait("Needs", "Docker")] public sealed class SqlServerRoundTripTests(SqlServer server) { diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json deleted file mode 100644 index 82eb3ce..0000000 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://xunit.net/schema/v3/xunit.runner.schema.json", - "parallelMode": "all" -}