From f5c3af68ab29804b137248d72a82ce90a84a113c Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 1 Oct 2026 21:27:38 +0100 Subject: [PATCH 1/4] EF-475: Migrate UnitTests to xunit v3 Move the xunit package reference out of tests/Directory.Build.props and into each test project so projects can migrate independently. UnitTests now uses xunit.v3; FunctionalTests and SpecificationTests stay on xunit 2.9.3 for now. Co-Authored-By: Claude Sonnet 5.5 --- tests/Directory.Build.props | 1 - .../MongoDB.EntityFrameworkCore.FunctionalTests.csproj | 4 ++++ .../MongoDB.EntityFrameworkCore.SpecificationTests.csproj | 4 ++++ .../MongoDB.EntityFrameworkCore.UnitTests.csproj | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 6b87b7669..7b407a8d4 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -13,7 +13,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj index 230fd539d..3de0e087e 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj @@ -24,6 +24,10 @@ + + + + diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj index 764ad5372..58901b15e 100644 --- a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj @@ -29,6 +29,10 @@ + + + + diff --git a/tests/MongoDB.EntityFrameworkCore.UnitTests/MongoDB.EntityFrameworkCore.UnitTests.csproj b/tests/MongoDB.EntityFrameworkCore.UnitTests/MongoDB.EntityFrameworkCore.UnitTests.csproj index 497d44e58..ff6eee5a3 100644 --- a/tests/MongoDB.EntityFrameworkCore.UnitTests/MongoDB.EntityFrameworkCore.UnitTests.csproj +++ b/tests/MongoDB.EntityFrameworkCore.UnitTests/MongoDB.EntityFrameworkCore.UnitTests.csproj @@ -24,6 +24,10 @@ + + + + From bc84fe7aa519957d2a968278f766d4a17c01c19e Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 1 Oct 2026 22:12:53 +0100 Subject: [PATCH 2/4] Migrate FunctionalTests to xunit v3 - Reference xunit.v3 instead of xunit 2.9.3 - IAsyncLifetime fixtures now return ValueTask - Custom Fact/Theory attributes set Skip in their constructor (Skip is no longer virtual) and pass through source information - Drop the Xunit.Abstractions usings (ITestOutputHelper now lives in Xunit) - Remove the Xunit.TestFramework assembly attribute; it named a type from an assembly that is not referenced anywhere - Suppress xUnit1051 for now (CancellationToken analyzer) Co-Authored-By: Claude Sonnet 5.5 --- .../Encryption/AutoEncryptionTests.cs | 15 +++++++++---- .../Encryption/AutoEncryptionTestsBase.cs | 21 +++++++++++++------ .../LoggingTests.cs | 1 - ...EntityFrameworkCore.FunctionalTests.csproj | 12 ++++------- .../CollectionSerializationTests.cs | 13 ++++++------ .../Utilities/AtlasFact.cs | 15 +++++++++---- .../AtlasTemporaryDatabaseFixture.cs | 2 +- .../Utilities/AtlasTheory.cs | 15 +++++++++---- .../Utilities/ReadOnlySampleGuidesFixture.cs | 6 +++--- .../Utilities/SampleGuidesFixture.cs | 4 ++-- .../Utilities/TemporaryDatabaseFixture.cs | 2 +- .../Utilities/TemporaryDatabaseFixtureBase.cs | 8 +++---- .../Utilities/TestContainersTestServer.cs | 6 +++--- .../Utilities/TestServer.cs | 9 ++++---- 14 files changed, 77 insertions(+), 52 deletions(-) diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTests.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTests.cs index 22512a9bc..a4df95d3f 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTests.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTests.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System.Runtime.CompilerServices; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Encryption; @@ -203,10 +204,16 @@ private BsonDocument CreatePatientEncryptionSchema() protected class EncryptionTheory : TheoryAttribute { - public override string? Skip - => TestServer.SupportsEncryption - ? null - : TestServer.EncryptionSkippedMessage; + public EncryptionTheory( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) + : base(sourceFilePath, sourceLineNumber) + { + if (!TestServer.SupportsEncryption) + { + Skip = TestServer.EncryptionSkippedMessage; + } + } } public enum EncryptionMode diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTestsBase.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTestsBase.cs index f81804f24..9854a8df3 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTestsBase.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Encryption/AutoEncryptionTestsBase.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System.Runtime.CompilerServices; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; @@ -48,12 +49,20 @@ protected MongoClient CreateEncryptedClient( protected class QueryableEncryptionTheory : TheoryAttribute { - public override string? Skip - => !TestServer.SupportsEncryption - ? TestServer.EncryptionSkippedMessage - : ShouldRunQueryableEncryptionTests - ? null - : "These Queryable Encryption tests require MongoDB 8.0 or later as declared by the VERSION environment variable."; + public QueryableEncryptionTheory( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) + : base(sourceFilePath, sourceLineNumber) + { + if (!TestServer.SupportsEncryption) + { + Skip = TestServer.EncryptionSkippedMessage; + } + else if (!ShouldRunQueryableEncryptionTests) + { + Skip = "These Queryable Encryption tests require MongoDB 8.0 or later as declared by the VERSION environment variable."; + } + } } protected static bool ShouldRunQueryableEncryptionTests => diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/LoggingTests.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/LoggingTests.cs index 13794fd0f..4fa97fdb3 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/LoggingTests.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/LoggingTests.cs @@ -19,7 +19,6 @@ using MongoDB.Driver; using MongoDB.EntityFrameworkCore.Diagnostics; using MongoDB.EntityFrameworkCore.FunctionalTests.Entities.Guides; -using Xunit.Abstractions; namespace MongoDB.EntityFrameworkCore.FunctionalTests; diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj index 3de0e087e..6cd2f0bd3 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj @@ -3,6 +3,9 @@ Debug EF8;Release EF8;Debug EF9;Release EF9;Debug EF10;Release EF10 AnyCPU + + $(NoWarn);xUnit1051 + TRACE;DEBUG;EF8 @@ -25,7 +28,7 @@ - + @@ -51,11 +54,4 @@ - - - - <_Parameter1>XUnit.Startup.Dispose.Hooks.TestFramework - <_Parameter2>XUnit.Startup.Dispose.Hooks - - diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Serialization/CollectionSerializationTests.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Serialization/CollectionSerializationTests.cs index 2f564ec14..10629d80a 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Serialization/CollectionSerializationTests.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Serialization/CollectionSerializationTests.cs @@ -126,14 +126,15 @@ class StringArrayEntity : BaseIdEntity public string[] aStringArray { get; set; } } - public static readonly TheoryData ArrayOfArraysData = - [ - [], - [ + public static readonly TheoryData ArrayOfArraysData = new() + { + Array.Empty(), + new[] + { new[] {"a", "b", "c"}, new[] {"d"} - ] - ]; + } + }; [Theory] [MemberData(nameof(ArrayOfArraysData))] diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasFact.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasFact.cs index 8b2895934..a3ba9fbe1 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasFact.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasFact.cs @@ -13,12 +13,19 @@ * limitations under the License. */ +using System.Runtime.CompilerServices; namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; public class AtlasFact : FactAttribute { - public override string? Skip - => TestServer.SupportsAtlas - ? null - : "Requires Atlas search capabilities."; + public AtlasFact( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) + : base(sourceFilePath, sourceLineNumber) + { + if (!TestServer.SupportsAtlas) + { + Skip = "Requires Atlas search capabilities."; + } + } } diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTemporaryDatabaseFixture.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTemporaryDatabaseFixture.cs index ccd521dd3..2fd42486f 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTemporaryDatabaseFixture.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTemporaryDatabaseFixture.cs @@ -29,7 +29,7 @@ public static async Task CreateInitializedAsync() public override TestServer TestServer => _server!; - public override async Task InitializeAsync() + public override async ValueTask InitializeAsync() { _server = await TestServer.GetOrInitializeTestServerAsync(MongoCondition.IsAtlas); await base.InitializeAsync(); diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTheory.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTheory.cs index 86bc9a348..9702d7e69 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTheory.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/AtlasTheory.cs @@ -13,12 +13,19 @@ * limitations under the License. */ +using System.Runtime.CompilerServices; namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; public class AtlasTheory : TheoryAttribute { - public override string? Skip - => TestServer.SupportsAtlas - ? null - : "Requires Atlas search capabilities."; + public AtlasTheory( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = -1) + : base(sourceFilePath, sourceLineNumber) + { + if (!TestServer.SupportsAtlas) + { + Skip = "Requires Atlas search capabilities."; + } + } } diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/ReadOnlySampleGuidesFixture.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/ReadOnlySampleGuidesFixture.cs index 8e053f029..70dce716f 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/ReadOnlySampleGuidesFixture.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/ReadOnlySampleGuidesFixture.cs @@ -30,7 +30,7 @@ public class ReadOnlySampleGuidesFixture : IAsyncLifetime public IMongoDatabase MongoDatabase { get; private set; } public IMongoClient Client { get; private set; } - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { var server = await TestServer.GetOrInitializeTestServerAsync(MongoCondition.None); Client = server.Client; @@ -38,6 +38,6 @@ public async Task InitializeAsync() SampleGuides.Populate(MongoDatabase); } - public Task DisposeAsync() - => Task.CompletedTask; + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; } diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/SampleGuidesFixture.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/SampleGuidesFixture.cs index ea22ba73a..2dc4879e8 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/SampleGuidesFixture.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/SampleGuidesFixture.cs @@ -25,7 +25,7 @@ public class SampleGuidesFixtureCollection : ICollectionFixture CreateInitializedAsync() public override TestServer TestServer => _server!; - public override async Task InitializeAsync() + public override async ValueTask InitializeAsync() { _server = await TestServer.GetOrInitializeTestServerAsync(MongoCondition.None); await base.InitializeAsync(); diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TemporaryDatabaseFixtureBase.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TemporaryDatabaseFixtureBase.cs index ff3d4273a..9ef898b82 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TemporaryDatabaseFixtureBase.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TemporaryDatabaseFixtureBase.cs @@ -104,12 +104,12 @@ private static string TransformNameIfNeeded(string? name) "Test was unable to determine a suitable collection name, please pass one to CreateTemporaryCollection"); } - public virtual Task InitializeAsync() + public virtual ValueTask InitializeAsync() { _mongoDatabase = Client.GetDatabase(TestDatabaseNamer.GetUniqueDatabaseName()); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public virtual Task DisposeAsync() - => Task.CompletedTask; + public virtual ValueTask DisposeAsync() + => ValueTask.CompletedTask; } diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestContainersTestServer.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestContainersTestServer.cs index cb84ab7b6..799b041fe 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestContainersTestServer.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestContainersTestServer.cs @@ -28,7 +28,7 @@ public override string ConnectionString public override MongoClient Client => _client!; - public override async Task InitializeAsync() + public override async ValueTask InitializeAsync() { // Dispose any container left over from a prior failed start before allocating a new one. if (_container != null) @@ -56,6 +56,6 @@ public override async Task InitializeAsync() _client = new(ConnectionString); } - public override Task DisposeAsync() - => _container == null ? Task.CompletedTask : _container.DisposeAsync().AsTask(); + public override ValueTask DisposeAsync() + => _container == null ? ValueTask.CompletedTask : _container.DisposeAsync(); } diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestServer.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestServer.cs index 59ac6371f..9960b5a4a 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestServer.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestServer.cs @@ -16,7 +16,6 @@ using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Core.Misc; -using Xunit.Abstractions; namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; @@ -170,9 +169,9 @@ public static readonly IMongoClient BrokenClient ConnectTimeout = TimeSpan.FromSeconds(1) }); - public virtual Task InitializeAsync() - => Task.CompletedTask; + public virtual ValueTask InitializeAsync() + => ValueTask.CompletedTask; - public virtual Task DisposeAsync() - => Task.CompletedTask; + public virtual ValueTask DisposeAsync() + => ValueTask.CompletedTask; } From 7fc04a5ce0f7202ccefba93464e8efa0f89222c2 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 6 Oct 2026 14:54:50 +0100 Subject: [PATCH 3/4] Decouple SpecificationTests from FunctionalTests and fix order-dependent test SpecificationTests must stay on xunit v2 (the EF Core specification test bases are built against it), but its project reference to FunctionalTests pulled xunit.v3 in transitively and broke the build with ambiguous types. - Remove the FunctionalTests project reference - Copy the test-server utilities it needs (TestServer, TestContainersTestServer, ExternalDatabaseTestServer, TestDatabaseNamer, MongoCondition and the Atlas container builder) into SpecificationTests, using the xunit v2 signatures - Make Shadow_property_can_be_used_for_rowversion bypass the model cache so it no longer depends on test order, which xunit v3 changed Co-Authored-By: Claude Sonnet 5.5 --- .../Mapping/ShadowPropertyTests.cs | 13 +- ...ityFrameworkCore.SpecificationTests.csproj | 1 - .../Utilities/ExternalDatabaseTestServer.cs | 26 +++ .../Utilities/MongoCondition.cs | 23 +++ .../Utilities/MongoDbAtlasBuilder.cs | 127 +++++++++++++ .../Utilities/MongoDbAtlasContainer.cs | 29 +++ .../Utilities/TestContainersTestServer.cs | 61 ++++++ .../Utilities/TestDatabaseNamer.cs | 27 +++ .../Utilities/TestServer.cs | 174 ++++++++++++++++++ 9 files changed, 478 insertions(+), 3 deletions(-) create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/ExternalDatabaseTestServer.cs create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoCondition.cs create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasBuilder.cs create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasContainer.cs create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestContainersTestServer.cs create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs create mode 100644 tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestServer.cs diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Mapping/ShadowPropertyTests.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Mapping/ShadowPropertyTests.cs index 5f2b74b05..64243145f 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Mapping/ShadowPropertyTests.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Mapping/ShadowPropertyTests.cs @@ -15,9 +15,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; +using MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; using MongoDB.EntityFrameworkCore.Metadata.Conventions; namespace MongoDB.EntityFrameworkCore.FunctionalTests.Mapping; @@ -321,12 +323,12 @@ public void Shadow_property_can_be_used_for_rowversion() { var staleAuthor = new Author {Name = "Damien"}; - var staleDb = new BloggingContext(For(database.MongoDatabase).Options, ModelConfiguration); + var staleDb = new BloggingContext(RowVersionOptions(), ModelConfiguration); staleDb.Authors.Add(staleAuthor); staleDb.SaveChanges(); { - var freshDb = new BloggingContext(For(database.MongoDatabase).Options, ModelConfiguration); + var freshDb = new BloggingContext(RowVersionOptions(), ModelConfiguration); var freshAuthor = freshDb.Authors.First(a => a.Id == staleAuthor.Id); freshAuthor.Name = "Damien Modified Fresh"; freshDb.SaveChanges(); @@ -335,6 +337,13 @@ public void Shadow_property_can_be_used_for_rowversion() staleAuthor.Name = "Damien Modified Stale"; Assert.Throws(() => staleDb.SaveChanges()); + // The model differs from other tests' BloggingContext model, so it must not share a cache entry. + DbContextOptions RowVersionOptions() + => For(database.MongoDatabase) + .ReplaceService() + .ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) + .Options; + void ModelConfiguration(ModelBuilder mb) => mb.Entity().Property("RowVersion").IsRowVersion(); } diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj index 58901b15e..00047811a 100644 --- a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj @@ -24,7 +24,6 @@ - diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/ExternalDatabaseTestServer.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/ExternalDatabaseTestServer.cs new file mode 100644 index 000000000..d0c833b2f --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/ExternalDatabaseTestServer.cs @@ -0,0 +1,26 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Driver; + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +public class ExternalDatabaseTestServer(string connectionString) : TestServer +{ + public override string ConnectionString + => connectionString; + + public override MongoClient Client { get; } = new(connectionString); +} diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoCondition.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoCondition.cs new file mode 100644 index 000000000..aacbacca5 --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoCondition.cs @@ -0,0 +1,23 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +[Flags] +public enum MongoCondition +{ + None = 0, + IsAtlas = 1 << 0 +} diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasBuilder.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasBuilder.cs new file mode 100644 index 000000000..1039ec793 --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasBuilder.cs @@ -0,0 +1,127 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Docker.DotNet.Models; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +public sealed class MongoDbAtlasBuilder : ContainerBuilder +{ + private const string MongoDbAtlasImage = "mongodb/mongodb-atlas-local:latest"; + public const ushort MongoDbAtlasPort = 27017; + + public MongoDbAtlasBuilder() + : this(new()) + => DockerResourceConfiguration = Init().DockerResourceConfiguration; + + private MongoDbAtlasBuilder(ContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + => DockerResourceConfiguration = resourceConfiguration; + + protected override ContainerConfiguration DockerResourceConfiguration { get; } + + public override MongoDbAtlasContainer Build() + { + Validate(); + + // The mongodb-atlas-local image starts mongod, then SIGTERMs and restarts it a couple of + // seconds later to inject the mongot/search wiring (mongotHost + searchIndexManagementHostAndPort). + // Until that restart completes, a vector query either hits the node mid-shutdown + // (MongoNodeIsRecoveringException, ShutdownInProgress) or finds mongot's query port (27027) not yet + // serving (Connection refused). Wait for the image's own healthcheck first - it only reports healthy + // after the restart and mongot wiring complete - then run the search-index probe as a final check. + var builder = WithWaitStrategy(Wait.ForUnixContainer() + .UntilContainerIsHealthy() + .AddCustomWaitStrategy(new WaitIndicateReadiness())); + + return new(builder.DockerResourceConfiguration); + } + + protected override MongoDbAtlasBuilder Init() + => base.Init() + .WithImage(MongoDbAtlasImage) + .WithPortBinding(MongoDbAtlasPort, true); + + protected override MongoDbAtlasBuilder Clone(IResourceConfiguration resourceConfiguration) + => Merge(DockerResourceConfiguration, new ContainerConfiguration(resourceConfiguration)); + + protected override MongoDbAtlasBuilder Clone(IContainerConfiguration resourceConfiguration) + => Merge(DockerResourceConfiguration, new ContainerConfiguration(resourceConfiguration)); + + protected override MongoDbAtlasBuilder Merge(IContainerConfiguration oldValue, IContainerConfiguration newValue) + => new(new ContainerConfiguration(oldValue, newValue)); + + + private sealed class WaitIndicateReadiness : IWaitUntil + { + public async Task UntilAsync(IContainer container) + { + var connectionString = ((MongoDbAtlasContainer)container).GetConnectionString(); + + using var client = new MongoClient(connectionString); + var databaseName = Guid.NewGuid().ToString(); + var ready = false; + + try + { + var database = client.GetDatabase(databaseName); + var collectionName = Guid.NewGuid().ToString(); + await database.CreateCollectionAsync(collectionName).ConfigureAwait(false); + + var model = new CreateSearchIndexModel( + Guid.NewGuid().ToString(), + SearchIndexType.VectorSearch, + BsonDocument.Parse( + """ + { + "fields": [ + { + "type": "vector", + "path": "Dummy", + "numDimensions": 8, + "similarity": "cosine" + } + ] + } + """)); + + var collection = database.GetCollection(collectionName); + await collection.SearchIndexes.CreateOneAsync(model).ConfigureAwait(false); + using var _ = await collection.SearchIndexes.ListAsync().ConfigureAwait(false); + ready = true; + } + catch (Exception ex) when (ex is MongoException or TimeoutException) + { + // Container/search-index service not yet ready - will be retried on the next poll. + } + + try + { + await client.DropDatabaseAsync(databaseName).ConfigureAwait(false); + } + catch (Exception ex) when (ex is MongoException or TimeoutException) + { + // Best-effort cleanup of the throwaway readiness database. + } + + return ready; + } + } +} diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasContainer.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasContainer.cs new file mode 100644 index 000000000..a0d6c04d3 --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/MongoDbAtlasContainer.cs @@ -0,0 +1,29 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +public sealed class MongoDbAtlasContainer(ContainerConfiguration configuration) : DockerContainer(configuration) +{ + public string GetConnectionString() + => new UriBuilder("mongodb", Hostname, GetMappedPublicPort(MongoDbAtlasBuilder.MongoDbAtlasPort)) + { + Query = "?directConnection=true" + } + .ToString(); +} diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestContainersTestServer.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestContainersTestServer.cs new file mode 100644 index 000000000..cb84ab7b6 --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestContainersTestServer.cs @@ -0,0 +1,61 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Driver; + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +public class TestContainersTestServer : TestServer +{ + private MongoClient? _client; + private MongoDbAtlasContainer? _container; + + public override string ConnectionString + => _container.GetConnectionString(); + + public override MongoClient Client + => _client!; + + public override async Task InitializeAsync() + { + // Dispose any container left over from a prior failed start before allocating a new one. + if (_container != null) + { + await _container.DisposeAsync().ConfigureAwait(false); + _container = null; + } + + var container = new MongoDbAtlasBuilder().Build(); + + try + { + // Bound startup so a stuck readiness probe (e.g. the Atlas Search Index Management + // service never coming up) fails fast instead of hanging the run indefinitely. + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + await container.StartAsync(cts.Token).ConfigureAwait(false); + } + catch + { + await container.DisposeAsync().ConfigureAwait(false); + throw; + } + + _container = container; + _client = new(ConnectionString); + } + + public override Task DisposeAsync() + => _container == null ? Task.CompletedTask : _container.DisposeAsync().AsTask(); +} diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs new file mode 100644 index 000000000..38f9ebd5c --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs @@ -0,0 +1,27 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +public static class TestDatabaseNamer +{ + public const string TestDatabasePrefix = "EFTest-"; + + private static readonly string TimeStamp = DateTime.Now.ToString("s").Replace(':', '-'); + private static int DbCount; + + public static string GetUniqueDatabaseName(string? staticName = null) + => $"{TestDatabasePrefix}{TimeStamp}{staticName}-{Interlocked.Increment(ref DbCount)}"; +} diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestServer.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestServer.cs new file mode 100644 index 000000000..8ec8f9ebb --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestServer.cs @@ -0,0 +1,174 @@ +/* Copyright 2023-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Misc; +using Xunit.Abstractions; + +namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; + +public abstract class TestServer : IAsyncLifetime +{ + private static readonly SemaphoreSlim AtlasSempahore = new(1); + private static TestServer? Default; + private static readonly SemaphoreSlim DefaultSempahore = new(1); + private static TestServer? Atlas; + + public static async Task GetOrInitializeTestServerAsync(MongoCondition requiredCondition) + { + if (requiredCondition.HasFlag(MongoCondition.IsAtlas)) + { + // Double-check locking; don't tell Java! + if (Atlas != null) + { + return Atlas; + } + + await AtlasSempahore.WaitAsync(); + try + { + if (Atlas == null) + { + Atlas = await GetOrInitialize("ATLAS_URI"); + } + + if (Atlas == null) + { + Atlas = await GetOrCreateDefault(); + } + + return Atlas; + } + finally + { + AtlasSempahore.Release(); + } + } + + return await GetOrCreateDefault(); + + static async Task GetOrCreateDefault() + { + // Double-check locking; don't tell Java! + if (Default != null) + { + return Default; + } + + await DefaultSempahore.WaitAsync(); + try + { + if (Default == null) + { + Default = await GetOrInitialize("MONGODB_URI"); + } + + return Default!; + } + finally + { + DefaultSempahore.Release(); + } + } + + static async Task GetOrInitialize(string uriName) + { + var uri = Environment.GetEnvironmentVariable(uriName); + if (string.IsNullOrWhiteSpace(uri)) + { + var containersServer = new TestContainersTestServer(); + await containersServer.InitializeAsync(); + return containersServer; + } + + if (uri.Equals("Disabled", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var externalServer = new ExternalDatabaseTestServer(uri); + await externalServer.InitializeAsync(); + + var databaseNameCursor = await externalServer.Client.ListDatabaseNamesAsync(); + while (await databaseNameCursor.MoveNextAsync()) + { + foreach (var databaseName in databaseNameCursor.Current) + { + if (databaseName.StartsWith(TestDatabaseNamer.TestDatabasePrefix)) + { + await externalServer.Client.DropDatabaseAsync(databaseName); + } + } + } + + return externalServer; + } + } + + public abstract string ConnectionString { get; } + public abstract MongoClient Client { get; } + + private SemanticVersion _serverVersion; + public SemanticVersion ServerVersion + => LazyInitializer.EnsureInitialized(ref _serverVersion, () => QueryServerVersion()); + + public bool SupportsBitwiseOperators + => ServerVersion >= new SemanticVersion(6, 3, 0); + + public static bool SupportsAtlas + { + get + { + var uri = Environment.GetEnvironmentVariable("ATLAS_URI"); + return uri == null || !uri.Equals("Disabled", StringComparison.OrdinalIgnoreCase); + } + } + + public static bool SupportsEncryption + => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CRYPT_SHARED_LIB_PATH")); + + public static bool SkipForAtlas(ITestOutputHelper testOutputHelper, string caller) + { + if (SupportsAtlas) + { + return false; + } + + testOutputHelper.WriteLine($"'{caller}' skipped because it required Atlas."); + return true; + } + + private SemanticVersion QueryServerVersion() + { + var database = Client.GetDatabase("__admin"); + var buildInfo = database.RunCommand(new BsonDocument("buildinfo", 1), ReadPreference.Primary); + return SemanticVersion.Parse(buildInfo["version"].AsString); + } + + public static readonly IMongoClient BrokenClient + = new MongoClient(new MongoClientSettings + { + Server = new MongoServerAddress("localhost", 27000), + ServerSelectionTimeout = TimeSpan.Zero, + ConnectTimeout = TimeSpan.FromSeconds(1) + }); + + public virtual Task InitializeAsync() + => Task.CompletedTask; + + public virtual Task DisposeAsync() + => Task.CompletedTask; +} From 3d6ec186a170ad471fd07fd585ece39ee1b4997e Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 21 Oct 2026 10:03:36 +0100 Subject: [PATCH 4/4] EF-475: Avoid database collisions between test assemblies and run tests sequentially on MongoDB 5.0/6.0 The xunit v3 migration decoupled SpecificationTests from FunctionalTests, and dotnet test on the solution now runs them concurrently against one server: - Give each assembly its own database name prefix that includes the EF version (EF-FunctionalTest-, EF-SpecTest-). Both assemblies previously generated identical names and their startup cleanup, which drops databases by prefix, dropped the other's live databases. - On MongoDB 5.0 and 6.0, overlapping writes intermittently fail with 'Unable to read from a snapshot due to pending collection catalog changes'. Run each test project in turn with sequential test collections on those servers only. This needs MONGODB_VERSION to be the matrix VERSION rather than the Evergreen version id. 7.0 and later keep the solution-level run. - Remove the assembly-level DisableTestParallelization from FunctionalTests. - Update AGENTS.md and the spec-conformance reviewer to match. --- .claude/agents/spec-conformance-reviewer.md | 6 +++--- AGENTS.md | 7 ++++--- evergreen/evergreen.yml | 8 ++++---- evergreen/run-tests.sh | 18 +++++++++++++++++- .../Usings.cs | 2 -- .../Utilities/TestDatabaseNamer.cs | 11 ++++++++++- .../AGENTS.md | 8 ++++---- .../Utilities/TestDatabaseNamer.cs | 11 ++++++++++- 8 files changed, 52 insertions(+), 19 deletions(-) diff --git a/.claude/agents/spec-conformance-reviewer.md b/.claude/agents/spec-conformance-reviewer.md index 68cfee9e8..4a5f87662 100644 --- a/.claude/agents/spec-conformance-reviewer.md +++ b/.claude/agents/spec-conformance-reviewer.md @@ -13,8 +13,8 @@ Read `tests/MongoDB.EntityFrameworkCore.SpecificationTests/AGENTS.md` first; the ## Review focus -- **Test parallelization is disabled by design.** `tests/.../FunctionalTests/Usings.cs` declares `[assembly: CollectionBehavior(DisableTestParallelization = true)]`. Shared MongoDB state and `[CallerMemberName]`-derived collection names both depend on serial execution. Don't enable parallel. -- **Per-test database isolation.** Tests get a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`. Tests using fixed names cross-pollute and produce intermittent failures. +- **Test concurrency is controlled per project and in CI.** `tests/.../SpecificationTests/Usings.cs` declares `[assembly: CollectionBehavior(DisableTestParallelization = true)]`; `FunctionalTests` (xUnit v3) does not and relies on unique per-fixture databases. Shared MongoDB state, `[CallerMemberName]`-derived collection names and MongoDB 5.0/6.0's snapshot errors (code 246) mean `evergreen/run-tests.sh` runs each test project in turn, with sequential collections, on those servers. Don't make CI run the projects concurrently there. +- **Per-test database isolation.** Tests get a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`; each test assembly has its own `TestDatabaseNamer` with a distinct prefix including the EF version (`EF-FunctionalTest-`, `EF-SpecTest-`) so concurrent processes never collide or clean up each other's databases. Tests using fixed names cross-pollute and produce intermittent failures. - **`[CallerMemberName]` collection-name generation.** `TemporaryDatabaseFixtureBase.CreateCollectionName(...)` derives collection names from caller method names. Hard-coded names defeat isolation. - **Specification-tests inheritance pattern.** Fixtures inherit `*FixtureBase` from `Microsoft.EntityFrameworkCore.Specification.Tests`; tests inherit `*TestBase`. Naming is `*MongoFixture` / `*MongoTest`. - **EF-version `#if` discipline.** Common patterns: `#if EF8` (legacy customizer interface, `Seed()` method), `#if EF8 || EF9` (pre-EF10 shapes), `#if !EF8 && !EF9` (EF10+ features). New tests must compile under all three define-constant configurations. @@ -32,6 +32,6 @@ See `.claude/agents/CONVENTIONS.md` for the report shape, tags, finding cap, and - Enabling test parallelization at the assembly level. - Skipping a Specification test without a clear "permanently unsupported" rationale. - Changes to `TestServer.GetOrInitializeTestServerAsync()` connection-priority logic. -- Changes to unique-name generation in `TestDatabaseNamer` (the `Interlocked.Increment`-backed `GetUniqueDatabaseName`) or the manual `DatabaseCleaner.CleanDatabase` skip-fact. +- Changes to unique-name generation or the per-assembly prefixes in either `TestDatabaseNamer` (the `Interlocked.Increment`-backed `GetUniqueDatabaseName`) or the manual `DatabaseCleaner.CleanDatabase` skip-fact. - New `[XUnitCollection]` that overlaps existing collection memberships in non-obvious ways. - Bumping the upstream EF version in `Versions.props` (touches every Specification-tests inheritance chain). diff --git a/AGENTS.md b/AGENTS.md index 4556751eb..07c329265 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ The MongoDB database provider for [Entity Framework Core](https://github.com/dot - Multi-EF-version targeting via build *configurations* (not target frameworks): `Debug|Release EF8`, `Debug|Release EF9`, `Debug|Release EF10`. EF8/EF9 build for `net8.0`; EF10 builds for `net10.0`. The active EF version is selected by the `EF8` / `EF9` / `EF10` define constant — see the version-conditional ``s in `src/MongoDB.EntityFrameworkCore/MongoDB.EntityFrameworkCore.csproj`. - EF and driver versions are pinned in `Versions.props`. The C# driver version can be overridden by setting the `DRIVER_VERSION` env var (CI uses this to test forward-compat). - `enable` on `src/`. `EF1001` — the provider intentionally consumes EF Core's internal APIs. -- xUnit + FluentAssertions for tests. Tests run **serially** — see `tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs` (`[assembly: CollectionBehavior(DisableTestParallelization = true)]`). +- xUnit + FluentAssertions for tests. `UnitTests` and `FunctionalTests` use xUnit v3 (`xunit.v3`); `SpecificationTests` stays on xUnit v2. `SpecificationTests` disables test parallelization at the assembly level (`Usings.cs`); `FunctionalTests` does not and relies on a unique database per fixture. CI runs the projects, and on MongoDB 5.0/6.0 the test collections, one at a time — see Testing. ## Project Structure - `src/MongoDB.EntityFrameworkCore/` — the provider. @@ -58,8 +58,9 @@ The provider follows EF Core's pattern, not the C# driver's. There is **no enfor ## Testing - **Recommended: run with both `MONGODB_URI` and `ATLAS_URI` unset.** `TestServer` then has TestContainers boot a `mongodb/mongodb-atlas-local` container, which (a) runs the **Atlas-gated tests (e.g. vector search) for real** against Atlas Search, and (b) gives each `dotnet test` process its **own isolated container** (random host port) plus uniquely-named databases — so multiple test runs, **even parallel agents, don't collide**. Trade-off: Docker is required and each run pays a one-time container boot (image ≈2 GB, pulled once). - Connection resolution (`tests/.../FunctionalTests/Utilities/TestServer.cs`): the *default* server comes from `MONGODB_URI` (else an atlas-local container); the *Atlas* (`IsAtlas`) server comes from `ATLAS_URI` (else an atlas-local container — so Atlas tests run whenever `ATLAS_URI` is not `"Disabled"`, regardless of `MONGODB_URI`). Set `ATLAS_URI="Disabled"` to skip Atlas tests; point either var at an external server to use it instead of a container (note: a plain `mongod`/replica set can't run Atlas Search, so Atlas tests against it won't be meaningful). -- Tests run **serially within a single test process** (parallelization disabled at the assembly level) — fixtures share global MongoDB state and each test relies on a uniquely-named database/collection that would race under intra-process parallelism. Across *separate* processes the per-process containers (above) keep runs isolated. -- Each test gets a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`. +- **Concurrency.** Each test relies on a uniquely-named database/collection and fixtures share global MongoDB state. `SpecificationTests` runs serially within its process (parallelization disabled at the assembly level); `FunctionalTests` may run test collections in parallel. Across *separate* processes the per-process containers (above) keep runs isolated. +- **MongoDB 5.0/6.0 snapshot errors.** On those servers overlapping writes intermittently fail with `Unable to read from a snapshot due to pending collection catalog changes` (server code 246): a transaction's snapshot is older than a collection created moments before. 7.0 and later were not affected. So `evergreen/run-tests.sh` runs each test project in turn with sequential test collections (`-- xUnit.ParallelizeTestCollections=false`) when `MONGODB_VERSION` is `5.0` or `6.0`, and runs the solution as usual otherwise. Don't run the test projects concurrently against a 5.0/6.0 server. +- Each test gets a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`. Each test assembly has its own copy with a distinct prefix that includes the EF version (`EF-FunctionalTest-`, `EF-SpecTest-`), so concurrent processes never collide and the startup cleanup (which drops databases by prefix) never drops another assembly's live databases. - `[ModuleInitializer]` in `tests/.../FunctionalTests/ModuleInitialization.cs` registers BSON serializers at load time. | Feature area | Required environment variables | diff --git a/evergreen/evergreen.yml b/evergreen/evergreen.yml index ab368b8c0..512c5f7cc 100644 --- a/evergreen/evergreen.yml +++ b/evergreen/evergreen.yml @@ -173,7 +173,7 @@ functions: OS=${OS} \ MONGODB_URI="${MONGODB_URI}" \ ATLAS_URI="${ATLAS_URI}" \ - MONGODB_VERSION="${version}" \ + MONGODB_VERSION="${VERSION}" \ CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ BUILD_CONFIGURATION="Debug EF8" \ ./evergreen/run-tests.sh @@ -187,7 +187,7 @@ functions: OS=${OS} \ MONGODB_URI="${MONGODB_URI}" \ ATLAS_URI="${ATLAS_URI}" \ - MONGODB_VERSION="${version}" \ + MONGODB_VERSION="${VERSION}" \ CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ BUILD_CONFIGURATION="Debug EF9" \ ./evergreen/run-tests.sh @@ -201,7 +201,7 @@ functions: OS=${OS} \ MONGODB_URI="${MONGODB_URI}" \ ATLAS_URI="${ATLAS_URI}" \ - MONGODB_VERSION="${version}" \ + MONGODB_VERSION="${VERSION}" \ CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ BUILD_CONFIGURATION="Debug EF10" \ ./evergreen/run-tests.sh @@ -217,7 +217,7 @@ functions: OS=${OS} \ MONGODB_URI="${MONGODB_URI}" \ ATLAS_URI="${ATLAS_URI}" \ - MONGODB_VERSION="${version}" \ + MONGODB_VERSION="${VERSION}" \ CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ BUILD_CONFIGURATION="Debug EF10" \ ./evergreen/run-tests.sh diff --git a/evergreen/run-tests.sh b/evergreen/run-tests.sh index deb57382f..ba456c32b 100644 --- a/evergreen/run-tests.sh +++ b/evergreen/run-tests.sh @@ -9,4 +9,20 @@ fi echo "Running EF Core tests for '${BUILD_CONFIGURATION}' configuration" dotnet clean "./MongoDB.EFCoreProvider.sln" -dotnet test "./MongoDB.EFCoreProvider.sln" -e MONGODB_URI="${MONGODB_URI}" -c "${BUILD_CONFIGURATION}" --results-directory "./artifacts/test-results/${BUILD_CONFIGURATION// /}" --logger "junit;LogFileName=TEST_{assembly}.xml;FailureBodyFormat=Verbose" --logger "console;verbosity=detailed" +test_args=(-e MONGODB_URI="${MONGODB_URI}" -c "${BUILD_CONFIGURATION}" --results-directory "./artifacts/test-results/${BUILD_CONFIGURATION// /}" --logger "junit;LogFileName=TEST_{assembly}.xml;FailureBodyFormat=Verbose" --logger "console;verbosity=detailed") + +case "${MONGODB_VERSION}" in + 5.0|6.0) + # MongoDB 5.0 and 6.0 intermittently fail with "Unable to read from a snapshot due to pending collection + # catalog changes" when writes overlap, so run each test project in turn (dotnet test on the solution runs + # them concurrently) with the test collections of each project running one at a time. + echo "Running test projects and test collections sequentially for MongoDB ${MONGODB_VERSION}" + exit_code=0 + for project in tests/*/*.csproj; do + dotnet test "./${project}" "${test_args[@]}" -- xUnit.ParallelizeTestCollections=false || exit_code=$? + done + exit $exit_code + ;; +esac + +dotnet test "./MongoDB.EFCoreProvider.sln" "${test_args[@]}" diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs index 39002423e..e553ae721 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs @@ -16,5 +16,3 @@ global using Xunit; global using XUnitCollection = Xunit.CollectionAttribute; global using MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; - -[assembly:CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestDatabaseNamer.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestDatabaseNamer.cs index 38f9ebd5c..7dad8cf85 100644 --- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestDatabaseNamer.cs +++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Utilities/TestDatabaseNamer.cs @@ -17,7 +17,16 @@ namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; public static class TestDatabaseNamer { - public const string TestDatabasePrefix = "EFTest-"; + // Distinct per assembly and EF version so concurrent test processes sharing a server never collide or clean up each other's databases. +#if EF8 + public const string TestDatabasePrefix = "EF8-FunctionalTest-"; +#elif EF9 + public const string TestDatabasePrefix = "EF9-FunctionalTest-"; +#elif EF10 + public const string TestDatabasePrefix = "EF10-FunctionalTest-"; +#else + public const string TestDatabasePrefix = "EF-FunctionalTest-"; +#endif private static readonly string TimeStamp = DateTime.Now.ToString("s").Replace(':', '-'); private static int DbCount; diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/AGENTS.md b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/AGENTS.md index d2707705e..2ab64e8b2 100644 --- a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/AGENTS.md +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/AGENTS.md @@ -14,15 +14,15 @@ This `AGENTS.md` covers the cross-cutting test infrastructure and the EF Core sp In: - **SpecificationTests project.** EF Core's standard provider-conformance suite. Test classes inherit upstream bases from `Microsoft.EntityFrameworkCore.Specification.Tests` (e.g. `NorthwindQueryFiltersQueryTestBase`) and override methods to assert the produced MQL via `AssertMql(...)`. The fixture pattern is `*MongoFixture` and the test class pattern is `*MongoTest`. -- **Functional test infrastructure** (`tests/.../FunctionalTests/Utilities/`) — `TestServer` (connection bootstrap from `ATLAS_URI` / `MONGODB_URI` / TestContainers fallback), `TemporaryDatabaseFixtureBase` / `TemporaryDatabaseFixture` (per-test database isolation), `TestDatabaseNamer` (unique names via `Interlocked.Increment`), `DatabaseCleaner` (a manual `[Fact(Skip = "...")]` opt-in cleaner — not automatic). `TestMqlLoggerFactory` lives in `tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/` (MQL capture by filtering `MongoEventId.ExecutedMqlQuery`). `ModuleInitialization` lives at the FunctionalTests project root (`tests/.../FunctionalTests/ModuleInitialization.cs`) and registers driver BSON serializers at module load via `[ModuleInitializer]`. -- **Parallelism / collection semantics** — `Usings.cs` declares `[assembly: CollectionBehavior(DisableTestParallelization = true)]` for FunctionalTests and SpecificationTests. Without this, shared MongoDB state and fixture/collection setup would race. +- **Test infrastructure.** `SpecificationTests` no longer references `FunctionalTests`; each project carries its own copy of the server bootstrap and database namer (`TestServer`, `TestContainersTestServer`, `ExternalDatabaseTestServer`, `TestDatabaseNamer`, …) — keep the copies in sync when changing one. In `tests/.../FunctionalTests/Utilities/`: `TestServer` (connection bootstrap from `ATLAS_URI` / `MONGODB_URI` / TestContainers fallback), `TemporaryDatabaseFixtureBase` / `TemporaryDatabaseFixture` (per-test database isolation), `TestDatabaseNamer` (unique names via `Interlocked.Increment`), `DatabaseCleaner` (a manual `[Fact(Skip = "...")]` opt-in cleaner — not automatic). `TestMqlLoggerFactory` lives in `tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/` (MQL capture by filtering `MongoEventId.ExecutedMqlQuery`). `ModuleInitialization` lives at the FunctionalTests project root (`tests/.../FunctionalTests/ModuleInitialization.cs`) and registers driver BSON serializers at module load via `[ModuleInitializer]`. +- **Parallelism / collection semantics** — `SpecificationTests/Usings.cs` (xUnit v2) declares `[assembly: CollectionBehavior(DisableTestParallelization = true)]`; `FunctionalTests` (xUnit v3) does not and relies on unique per-fixture databases. On MongoDB 5.0/6.0 CI additionally runs the test projects, and their collections, one at a time (`evergreen/run-tests.sh`) because overlapping writes cause intermittent snapshot errors. Out: per-area test classes themselves (`Query/`, `Storage/`, etc.) — those are owned by their respective `src/` area reviewers. ## Test infrastructure - **Connection bootstrap** (`tests/.../FunctionalTests/Utilities/TestServer.cs`). The *default* server resolves from `MONGODB_URI` (else a `TestContainersTestServer`); the *Atlas* (`IsAtlas`) server resolves from `ATLAS_URI` (else a `TestContainersTestServer`). `TestContainersTestServer` boots **`mongodb/mongodb-atlas-local`** (Atlas-capable, with the Search Index Management service), not a plain `mongod` — so Atlas tests run for real against a container. `ATLAS_URI="Disabled"` skips Atlas tests; an external `ATLAS_URI`/`MONGODB_URI` uses that server instead. Cached once via double-checked locking, so each test *process* boots its own container(s) on a random host port. **Recommended: leave both `MONGODB_URI` and `ATLAS_URI` unset** — the whole run is then self-contained on atlas-local, Atlas tests run genuinely, and separate processes (parallel agents) are isolated. -- **Per-test isolation.** `TemporaryDatabaseFixtureBase.InitializeAsync()` requests a unique database name from `TestDatabaseNamer.GetUniqueDatabaseName()` (which appends an `Interlocked.Increment` counter to a timestamped prefix). `DisposeAsync` returns `Task.CompletedTask` — there is no automatic teardown; stale `Test*` databases are cleaned up only by manually running the `[Fact(Skip = "Manually run to clean up the database")]` `DatabaseCleaner.CleanDatabase` test. Test methods get a *collection name* derived from `[CallerMemberName]` (`CreateCollectionName(...)`) with a sequential-counter fallback for CI where caller names may be unavailable. +- **Per-test isolation.** `TemporaryDatabaseFixtureBase.InitializeAsync()` requests a unique database name from `TestDatabaseNamer.GetUniqueDatabaseName()` (an assembly- and EF-version-specific prefix such as `EF10-FunctionalTest-` / `EF10-SpecTest-`, a timestamp and an `Interlocked.Increment` counter; the startup cleanup only drops databases with its own assembly's prefix). `DisposeAsync` returns `Task.CompletedTask` — there is no automatic teardown; stale `Test*` databases are cleaned up only by manually running the `[Fact(Skip = "Manually run to clean up the database")]` `DatabaseCleaner.CleanDatabase` test. Test methods get a *collection name* derived from `[CallerMemberName]` (`CreateCollectionName(...)`) with a sequential-counter fallback for CI where caller names may be unavailable. - **Fixture sharing.** Heavy fixtures are declared with `[CollectionDefinition("name")]` and consumed via `[XUnitCollection("name")]`. Encryption tests live in their own collection (`[XUnitCollection("Encryption")]`) so they serialize cleanly with the crypto state. Compatibility tests likewise. - **MQL assertion pattern.** Specification tests override `await base.SomeTest()` and follow with `AssertMql("{ $match: ... }", "{ $project: ... }")` (see e.g. `NorthwindQueryFiltersQueryMongoTest`). The pipeline-string format is whitespace-insensitive but field-order-sensitive. @@ -58,7 +58,7 @@ Special test concerns under `FunctionalTests/`: `Encryption/` (CSFLE / QE end-to ## Common pitfalls -- **Don't enable test parallelization.** Tests share global MongoDB state and rely on serial execution for stable fixture/collection setup; parallel runs cross-pollute. +- **Don't make the Specification tests parallel, and don't run the test projects concurrently against a MongoDB 5.0/6.0 server.** Tests share global MongoDB state and rely on serial execution for stable fixture/collection setup, and overlapping writes on 5.0/6.0 produce `Unable to read from a snapshot due to pending collection catalog changes` (code 246). - **Don't drop test isolation.** A test that uses a fixed collection name (instead of `[CallerMemberName]`) collides with anything else using that name across the suite — the failure mode is intermittent leaks. - **Encryption tests skip silently** when `CRYPT_SHARED_LIB_PATH` is unset (`SupportsEncryption` returns false). The test run prints a warning when this happens. To enable them, run `eval "$(tests/tools/fetch-crypt-shared.sh)"` (exports both `CRYPT_SHARED_LIB_PATH` and `MONGODB_BINARIES`; see `CONTRIBUTING.md`). - **MQL assertions are field-order-sensitive.** When you add a new translator branch, expected-MQL strings often need updates across many specification tests. diff --git a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs index 38f9ebd5c..c217b4220 100644 --- a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs @@ -17,7 +17,16 @@ namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities; public static class TestDatabaseNamer { - public const string TestDatabasePrefix = "EFTest-"; + // Distinct per assembly and EF version so concurrent test processes sharing a server never collide or clean up each other's databases. +#if EF8 + public const string TestDatabasePrefix = "EF8-SpecTest-"; +#elif EF9 + public const string TestDatabasePrefix = "EF9-SpecTest-"; +#elif EF10 + public const string TestDatabasePrefix = "EF10-SpecTest-"; +#else + public const string TestDatabasePrefix = "EF-SpecTest-"; +#endif private static readonly string TimeStamp = DateTime.Now.ToString("s").Replace(':', '-'); private static int DbCount;