-
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 3 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
This file was deleted.
This file was deleted.
| 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,115 @@ public static class TestEnvironment | |
| .Build() | ||
| .GetSection("Test:Cosmos"); | ||
|
|
||
| public static string DefaultConnection { get; } = string.IsNullOrEmpty(Config["DefaultConnection"]) | ||
| ? "https://localhost:8081" | ||
| : Config["DefaultConnection"]; | ||
| private static CosmosDbContainer _container; | ||
| private static bool _initialized; | ||
| private static readonly SemaphoreSlim _initSemaphore = new(1, 1); | ||
|
|
||
| public static string DefaultConnection { get; private set; } | ||
|
AndriySvyryd marked this conversation as resolved.
Outdated
|
||
|
|
||
| internal static HttpMessageHandler HttpMessageHandler { get; private set; } | ||
|
|
||
| public static async Task InitializeAsync() | ||
| { | ||
| if (_initialized) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| await _initSemaphore.WaitAsync().ConfigureAwait(false); | ||
|
|
||
| try | ||
| { | ||
| if (_initialized) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // If a connection string is specified (env var, config.json...), always use that. | ||
| var configured = Config["DefaultConnection"]; | ||
| if (!string.IsNullOrEmpty(configured)) | ||
| { | ||
| DefaultConnection = configured; | ||
| _initialized = true; | ||
| return; | ||
| } | ||
|
|
||
| // Try to connect to the default emulator endpoint. | ||
|
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. My preferred flow here would be simpler:
This way, if I want to have an always-on emulator running in docker (or the Windows emulator), it's simply up to me to define the environment variable, rather than rely on probing here with a hard-coded port etc. Am I missing something, why this more complicated logic is needed?
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 d2b56b1. Removed localhost probing entirely. The flow is now:
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. The above approach was simpler for the developer. If they had the emulator running they didn't need to even know about the environment variable.
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. Sure, but I'm pretty sure that if someone is taking the trouble to install the emulator locally, just adding an environment variable wouldn't represent an issue (plus if the testcontainer works well, there won't be a huge reason to run a permanent instance anyway). It also doesn't help that the TCP port here is very generic (8081), and some other service could be running on it, generating a weird and unexpected error (i.e. if this were some very standard port reserved for Cosmos I'd be more inclined to like it).
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. But I'll add that if you have strong feelings here I'm fine going either way.
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. Until the Linux emulator gets feature parity this will be the main way to test Cosmos locally on Windows.
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 2aa85dd. Restored localhost probing: the flow is now (1) configured endpoint from env var/config, (2) probe
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.
That's still basically just you :) And adding an environment variable for that seems very reasonable. But no problem. |
||
| if (await TryProbeEmulatorAsync("https://localhost:8081").ConfigureAwait(false)) | ||
| { | ||
| DefaultConnection = "https://localhost:8081"; | ||
| _initialized = true; | ||
| return; | ||
| } | ||
|
|
||
| // Try to start a testcontainer with the Linux emulator. | ||
| try | ||
| { | ||
| _container = new CosmosDbBuilder("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") | ||
| .Build(); | ||
| await _container.StartAsync().ConfigureAwait(false); | ||
|
AndriySvyryd marked this conversation as resolved.
Outdated
|
||
|
|
||
| AppDomain.CurrentDomain.ProcessExit += (_, _) => | ||
| { | ||
| 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. | ||
| } | ||
| }; | ||
|
|
||
| DefaultConnection = new UriBuilder( | ||
| Uri.UriSchemeHttp, | ||
| _container.Hostname, | ||
| _container.GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)).ToString(); | ||
| HttpMessageHandler = _container.HttpMessageHandler; | ||
| } | ||
| catch when (!SkipConnectionCheck) | ||
| { | ||
| // Any failure (Docker not installed, daemon not running, image pull failure, etc.) | ||
|
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. I don't think we should do this. As above, things should IMHO be super simple, and we shouldn't try to fall back from failures to a default endpoint - that only makes things more difficult to figure out if there's a failure. My preference would be to just always connect to the environment variable endpoint if the variable is defined (failing if there's an issue), or if the variable isn't defined, just always start a testcontainer (failing if there's an issue).
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 d2b56b1. Removed the
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 should still add a helpful message saying that either an emulator or docker needs to be running, or an external connection string should be provided.
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. Yep, agreed. @copilot can you add that please?
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 b0ae4a8. Wrapped the testcontainer startup in a try/catch that throws an |
||
| // falls back to the default endpoint. The connection check in CosmosTestStore will | ||
| // determine whether the emulator is actually reachable and skip tests if not. | ||
| DefaultConnection = "https://localhost:8081"; | ||
| } | ||
|
|
||
| _initialized = true; | ||
| } | ||
| finally | ||
| { | ||
| _initSemaphore.Release(); | ||
| } | ||
| } | ||
|
|
||
| private static async Task<bool> TryProbeEmulatorAsync(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 = await client.GetAsync(endpoint).ConfigureAwait(false); | ||
| 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 +146,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 => _container != null | ||
| ? "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.