-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Auto-start Cosmos emulator via Testcontainers #37999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
ead33ec
09480a5
6df36b8
5163459
0fa9387
844bf18
b42e171
eff2931
a322cbe
3d67e46
d2b56b1
b0ae4a8
2aa85dd
8ea3913
3a44583
91b538a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| using Azure.Core; | ||
| using Azure.Identity; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Testcontainers.CosmosDb; | ||
|
|
||
| namespace Microsoft.EntityFrameworkCore.TestUtilities; | ||
|
|
||
|
|
@@ -22,15 +23,96 @@ public static class TestEnvironment | |
| .Build() | ||
| .GetSection("Test:Cosmos"); | ||
|
|
||
| public static string DefaultConnection { get; } = string.IsNullOrEmpty(Config["DefaultConnection"]) | ||
| ? "https://localhost:8081" | ||
| : Config["DefaultConnection"]; | ||
| private static readonly Lazy<(string Connection, CosmosDbContainer Container)> _connectionInfo = new(InitializeConnection); | ||
|
|
||
| public static string DefaultConnection => _connectionInfo.Value.Connection; | ||
|
|
||
| private static CosmosDbContainer Container => _connectionInfo.Value.Container; | ||
|
|
||
| public static bool IsTestContainer => Container != null; | ||
|
AndriySvyryd marked this conversation as resolved.
Outdated
|
||
|
|
||
| internal static Func<HttpMessageHandler> HttpMessageHandlerFactory | ||
| => Container != null ? () => Container.HttpMessageHandler : null; | ||
|
AndriySvyryd marked this conversation as resolved.
Outdated
|
||
|
|
||
| private static (string Connection, CosmosDbContainer Container) InitializeConnection() | ||
|
AndriySvyryd marked this conversation as resolved.
Outdated
|
||
| { | ||
| // If a connection string is specified (env var, config.json...), always use that. | ||
| var configured = Config["DefaultConnection"]; | ||
| if (!string.IsNullOrEmpty(configured)) | ||
| { | ||
| return (configured, null); | ||
| } | ||
|
|
||
| // Try to connect to the default emulator endpoint. | ||
| if (TryProbeEmulator("https://localhost:8081")) | ||
| { | ||
| return ("https://localhost:8081", null); | ||
| } | ||
|
|
||
| // Try to start a testcontainer with the Linux emulator. | ||
| // Synchronous blocking is required here because this runs in a Lazy<T> initializer | ||
| // which cannot be async. This matches the pattern used in SQL Server's TestEnvironment. | ||
| try | ||
| { | ||
| var container = new CosmosDbBuilder("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") | ||
| .Build(); | ||
| container.StartAsync().GetAwaiter().GetResult(); | ||
|
|
||
| AppDomain.CurrentDomain.ProcessExit += (_, _) => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'd ideally use an xunit assembly fixture to start and stop the container.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot use an xunit assembly fixture to start and stop the container.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 8ea3913. Implemented a custom xunit test framework ( The |
||
| { | ||
| try | ||
| { | ||
| container.DisposeAsync().AsTask().GetAwaiter().GetResult(); | ||
| } | ||
| catch | ||
| { | ||
| // Best-effort cleanup: container may already be stopped or Docker daemon | ||
| // may have exited before the process exit handler runs. | ||
| } | ||
| }; | ||
|
|
||
| var endpoint = new UriBuilder( | ||
| Uri.UriSchemeHttp, | ||
| container.Hostname, | ||
| container.GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)).ToString(); | ||
|
|
||
| return (endpoint, container); | ||
| } | ||
| catch | ||
| { | ||
| // Any failure (Docker not installed, daemon not running, image pull failure, etc.) | ||
|
AndriySvyryd marked this conversation as resolved.
Outdated
|
||
| // falls back to the default endpoint. The connection check in CosmosTestStore will | ||
| // determine whether the emulator is actually reachable and skip tests if not. | ||
| return ("https://localhost:8081", null); | ||
| } | ||
| } | ||
|
|
||
| private static bool TryProbeEmulator(string endpoint) | ||
| { | ||
| try | ||
| { | ||
| using var handler = new HttpClientHandler | ||
| { | ||
| ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator | ||
| }; | ||
| using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(3) }; | ||
| // Any successful response (even 401) means the emulator is up and accepting connections. | ||
| using var response = client.GetAsync(endpoint).GetAwaiter().GetResult(); | ||
| return true; | ||
| } | ||
| catch | ||
| { | ||
| // Expected: HttpRequestException (connection refused), TaskCanceledException (timeout), | ||
| // or SocketException when the emulator is not running. | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| public static string AuthToken { get; } = string.IsNullOrEmpty(Config["AuthToken"]) | ||
| ? _emulatorAuthToken | ||
| : Config["AuthToken"]; | ||
|
|
||
| public static string ConnectionString { get; } = $"AccountEndpoint={DefaultConnection};AccountKey={AuthToken}"; | ||
| public static string ConnectionString => $"AccountEndpoint={DefaultConnection};AccountKey={AuthToken}"; | ||
|
|
||
| public static bool UseTokenCredential { get; } = string.Equals(Config["UseTokenCredential"], "true", StringComparison.OrdinalIgnoreCase); | ||
|
|
||
|
|
@@ -45,12 +127,14 @@ public static class TestEnvironment | |
| ? AzureLocation.WestUS | ||
| : Enum.Parse<AzureLocation>(Config["AzureLocation"]); | ||
|
|
||
| public static bool IsEmulator { get; } = !UseTokenCredential && (AuthToken == _emulatorAuthToken); | ||
| public static bool IsEmulator => !UseTokenCredential && (AuthToken == _emulatorAuthToken); | ||
|
|
||
| public static bool SkipConnectionCheck { get; } = string.Equals(Config["SkipConnectionCheck"], "true", StringComparison.OrdinalIgnoreCase); | ||
|
|
||
| public static string EmulatorType { get; } = Config["EmulatorType"] ?? (!OperatingSystem.IsWindows() ? "linux" : ""); | ||
| public static string EmulatorType => IsTestContainer | ||
| ? "linux" | ||
| : Config["EmulatorType"] ?? (!OperatingSystem.IsWindows() ? "linux" : ""); | ||
|
|
||
| public static bool IsLinuxEmulator { get; } = IsEmulator | ||
| public static bool IsLinuxEmulator => IsEmulator | ||
| && EmulatorType.Equals("linux", StringComparison.OrdinalIgnoreCase); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.