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/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/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/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.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/MongoDB.EntityFrameworkCore.FunctionalTests.csproj index 230fd539d..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 @@ -24,6 +27,10 @@ + + + + @@ -47,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/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/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/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.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; } 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/MongoDB.EntityFrameworkCore.SpecificationTests.csproj b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj index 764ad5372..00047811a 100644 --- a/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/MongoDB.EntityFrameworkCore.SpecificationTests.csproj @@ -24,11 +24,14 @@ - + + + + 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..c217b4220 --- /dev/null +++ b/tests/MongoDB.EntityFrameworkCore.SpecificationTests/Utilities/TestDatabaseNamer.cs @@ -0,0 +1,36 @@ +/* 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 +{ + // 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; + + 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; +} 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 @@ + + + +