Skip to content

Commit 6df36b8

Browse files
CopilotAndriySvyryd
andcommitted
Address review feedback: async init, SkipConnectionCheck throw, restore helix/copilot env vars, delete scripts
- Make TestEnvironment initialization fully async (InitializeAsync) - Set DefaultConnection and HttpMessageHandler values during async init - If SkipConnectionCheck=true, let testcontainer failures throw - Create HttpClient instance outside the lambda in ApplyConfiguration - Inline IsTestContainer (removed property) - Restore copilot-setup-steps.yml env vars (EmulatorType, DefaultConnection, SkipConnectionCheck) - Restore helix.proj Windows.11 block and SkipConnectionCheck for Ubuntu - Delete eng/testing/run-cosmos-container.{sh,ps1} - Update SKILL.md Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/7f2ba766-d1b7-484b-899c-4b6d320a97e5
1 parent 09480a5 commit 6df36b8

8 files changed

Lines changed: 93 additions & 177 deletions

File tree

.agents/skills/cosmos-provider/SKILL.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ Non-relational provider with its own parallel query pipeline. Uses JSON for docu
2525

2626
### Automatic Testcontainer Startup
2727

28-
Cosmos functional tests automatically manage the emulator lifecycle via [Testcontainers](https://testcontainers.com/modules/cosmodb/?language=dotnet) (`Testcontainers.CosmosDb` NuGet package). The initialization logic in `TestEnvironment.cs` follows this order:
28+
Cosmos functional tests automatically manage the emulator lifecycle via [Testcontainers](https://testcontainers.com/modules/cosmodb/?language=dotnet) (`Testcontainers.CosmosDb` NuGet package). The async initialization logic in `TestEnvironment.InitializeAsync()` (called from `CosmosTestStore.IsConnectionAvailableAsync()`) follows this order:
2929

3030
1. **Configured endpoint**: If `Test__Cosmos__DefaultConnection` env var (or `cosmosConfig.json` / `cosmosConfig.test.json`) is set, it is used directly — no container is started.
3131
2. **Local emulator probe**: A quick HTTPS probe is sent to `https://localhost:8081`. If a running emulator responds, it is used.
3232
3. **Testcontainer fallback**: If neither of the above succeeds, a `CosmosDbContainer` is started with the Linux emulator image (`mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview`). The container is disposed on process exit.
33-
4. **Graceful skip**: If Docker is unavailable and no emulator is reachable, the default endpoint is used and `IsConnectionAvailableAsync()` returns `false`, causing tests to be skipped.
33+
4. **Graceful skip**: If Docker is unavailable and no emulator is reachable, the default endpoint is used and `IsConnectionAvailableAsync()` returns `false`, causing tests to be skipped. However, if `Test__Cosmos__SkipConnectionCheck=true` is set (e.g. in CI), the testcontainer failure is **not** caught so that infrastructure problems surface immediately.
3434

3535
### Linux Emulator Detection
3636

@@ -43,15 +43,11 @@ The Linux (vnext) emulator does **not** support transactional batches, so `Linux
4343

4444
### HttpClient Handling
4545

46-
When a testcontainer is active, `CosmosDbContextOptionsBuilderExtensions.ApplyConfiguration` uses the container's `HttpMessageHandler` (a URI rewriter that routes requests to the mapped container port over HTTP). When connecting to a local HTTPS emulator, it uses `DangerousAcceptAnyServerCertificateValidator` instead.
47-
48-
### Manual Scripts (Legacy)
49-
50-
The shell scripts `eng/testing/run-cosmos-container.sh` and `eng/testing/run-cosmos-container.ps1` can still be used to manually start the emulator in Docker when needed, but they are no longer invoked by Helix or CI.
46+
When a testcontainer is active, `CosmosDbContextOptionsBuilderExtensions.ApplyConfiguration` uses the container's `HttpMessageHandler` (a URI rewriter that routes requests to the mapped container port over HTTP), captured once during initialization. When connecting to a local HTTPS emulator, it uses `DangerousAcceptAnyServerCertificateValidator` instead.
5147

5248
### Key Files
5349

54-
- `test/EFCore.Cosmos.FunctionalTests/TestUtilities/TestEnvironment.cs` — connection auto-detection and testcontainer lifecycle
50+
- `test/EFCore.Cosmos.FunctionalTests/TestUtilities/TestEnvironment.cs`async connection auto-detection and testcontainer lifecycle
5551
- `test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs` — test store creation, seeding, cleanup
5652
- `test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosDbContextOptionsBuilderExtensions.cs` — shared Cosmos options (execution strategy, timeout, HttpClient, Gateway mode)
5753
- `test/EFCore.Cosmos.FunctionalTests/TestUtilities/LinuxEmulatorSaveChangesInterceptor.cs` — disables transactional batches for the Linux emulator

.github/workflows/copilot-setup-steps.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,8 @@ jobs:
6363
- name: Export environment variables for the agent's session
6464
run: |
6565
echo "Test__SqlServer__DefaultConnection=Server=localhost;Database=test;User=SA;Password=PLACEHOLDERPass$$w0rd;Connect Timeout=60;ConnectRetryCount=0;Trust Server Certificate=true" >> "$GITHUB_ENV"
66+
echo "Test__Cosmos__DefaultConnection=https://localhost:8081" >> "$GITHUB_ENV"
67+
echo "Test__Cosmos__EmulatorType=linux" >> "$GITHUB_ENV"
68+
echo "Test__Cosmos__SkipConnectionCheck=true" >> "$GITHUB_ENV"
6669
echo "DOTNET_ROOT=$PWD/.dotnet/" >> "$GITHUB_ENV"
6770
echo "$PWD/.dotnet/" >> $GITHUB_PATH

eng/helix.proj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@
6565
</XUnitProject>
6666
</ItemGroup>
6767

68+
<ItemGroup Condition = "'$(HelixTargetQueue.StartsWith(`Windows.11`))'">
69+
<XUnitProject Update="$(CosmosTests)">
70+
<PreCommands>$(PreCommands); set Test__Cosmos__SkipConnectionCheck=true</PreCommands>
71+
</XUnitProject>
72+
</ItemGroup>
73+
6874
<!-- Start SqlServer instance for test projects which uses SqlServer on docker. Only run SqlServer tests. -->
6975
<ItemGroup Condition = "'$(HelixTargetQueue.Contains(`helix-sqlserver`))'">
7076
<XUnitProject Remove="$(RepoRoot)/test/**/*.csproj"/>
@@ -78,7 +84,9 @@
7884
<ItemGroup Condition = "'$(HelixTargetQueue)' == 'Ubuntu.2204.Amd64.XL.Open' OR '$(HelixTargetQueue)' == 'Ubuntu.2204.Amd64.XL'">
7985
<XUnitProject Remove="$(RepoRoot)/test/**/*.csproj"/>
8086
<XUnitProject Remove="$(RepoRoot)/test/**/*.fsproj"/>
81-
<XUnitProject Include="$(CosmosTests)" />
87+
<XUnitProject Include="$(CosmosTests)">
88+
<PreCommands>$(PreCommands); export Test__Cosmos__SkipConnectionCheck=true</PreCommands>
89+
</XUnitProject>
8290
</ItemGroup>
8391

8492
<!-- Run tests that don't need SqlServer or Cosmos on bare Ubuntu -->

eng/testing/run-cosmos-container.ps1

Lines changed: 0 additions & 61 deletions
This file was deleted.

eng/testing/run-cosmos-container.sh

Lines changed: 0 additions & 51 deletions
This file was deleted.

test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosDbContextOptionsBuilderExtensions.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ public static class CosmosDbContextOptionsBuilderExtensions
99
{
1010
public static CosmosDbContextOptionsBuilder ApplyConfiguration(this CosmosDbContextOptionsBuilder optionsBuilder)
1111
{
12-
var handlerFactory = TestEnvironment.HttpMessageHandlerFactory;
12+
var httpClient = TestEnvironment.HttpMessageHandler != null
13+
? new HttpClient(TestEnvironment.HttpMessageHandler)
14+
: new HttpClient(
15+
new HttpClientHandler
16+
{
17+
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
18+
});
1319

1420
optionsBuilder
1521
.ExecutionStrategy(d => new TestCosmosExecutionStrategy(d))
1622
.RequestTimeout(TimeSpan.FromMinutes(20))
17-
.HttpClientFactory(handlerFactory != null
18-
? () => new HttpClient(handlerFactory())
19-
: () => new HttpClient(
20-
new HttpClientHandler
21-
{
22-
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
23-
}))
23+
.HttpClientFactory(() => httpClient)
2424
.ConnectionMode(ConnectionMode.Gateway);
2525

2626
return optionsBuilder;

test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuild
105105

106106
public static async ValueTask<bool> IsConnectionAvailableAsync()
107107
{
108+
await TestEnvironment.InitializeAsync().ConfigureAwait(false);
109+
108110
if (TestEnvironment.SkipConnectionCheck)
109111
{
110112
return true;

test/EFCore.Cosmos.FunctionalTests/TestUtilities/TestEnvironment.cs

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,71 +23,90 @@ public static class TestEnvironment
2323
.Build()
2424
.GetSection("Test:Cosmos");
2525

26-
private static readonly Lazy<(string Connection, CosmosDbContainer Container)> _connectionInfo = new(InitializeConnection);
26+
private static CosmosDbContainer _container;
27+
private static bool _initialized;
28+
private static readonly SemaphoreSlim _initSemaphore = new(1, 1);
2729

28-
public static string DefaultConnection => _connectionInfo.Value.Connection;
30+
public static string DefaultConnection { get; private set; }
2931

30-
private static CosmosDbContainer Container => _connectionInfo.Value.Container;
32+
internal static HttpMessageHandler HttpMessageHandler { get; private set; }
3133

32-
public static bool IsTestContainer => Container != null;
33-
34-
internal static Func<HttpMessageHandler> HttpMessageHandlerFactory
35-
=> Container != null ? () => Container.HttpMessageHandler : null;
36-
37-
private static (string Connection, CosmosDbContainer Container) InitializeConnection()
34+
public static async Task InitializeAsync()
3835
{
39-
// If a connection string is specified (env var, config.json...), always use that.
40-
var configured = Config["DefaultConnection"];
41-
if (!string.IsNullOrEmpty(configured))
36+
if (_initialized)
4237
{
43-
return (configured, null);
38+
return;
4439
}
4540

46-
// Try to connect to the default emulator endpoint.
47-
if (TryProbeEmulator("https://localhost:8081"))
48-
{
49-
return ("https://localhost:8081", null);
50-
}
41+
await _initSemaphore.WaitAsync().ConfigureAwait(false);
5142

52-
// Try to start a testcontainer with the Linux emulator.
53-
// Synchronous blocking is required here because this runs in a Lazy<T> initializer
54-
// which cannot be async. This matches the pattern used in SQL Server's TestEnvironment.
5543
try
5644
{
57-
var container = new CosmosDbBuilder("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview")
58-
.Build();
59-
container.StartAsync().GetAwaiter().GetResult();
45+
if (_initialized)
46+
{
47+
return;
48+
}
6049

61-
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
50+
// If a connection string is specified (env var, config.json...), always use that.
51+
var configured = Config["DefaultConnection"];
52+
if (!string.IsNullOrEmpty(configured))
6253
{
63-
try
64-
{
65-
container.DisposeAsync().AsTask().GetAwaiter().GetResult();
66-
}
67-
catch
68-
{
69-
// Best-effort cleanup: container may already be stopped or Docker daemon
70-
// may have exited before the process exit handler runs.
71-
}
72-
};
54+
DefaultConnection = configured;
55+
_initialized = true;
56+
return;
57+
}
7358

74-
var endpoint = new UriBuilder(
75-
Uri.UriSchemeHttp,
76-
container.Hostname,
77-
container.GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)).ToString();
59+
// Try to connect to the default emulator endpoint.
60+
if (await TryProbeEmulatorAsync("https://localhost:8081").ConfigureAwait(false))
61+
{
62+
DefaultConnection = "https://localhost:8081";
63+
_initialized = true;
64+
return;
65+
}
66+
67+
// Try to start a testcontainer with the Linux emulator.
68+
try
69+
{
70+
_container = new CosmosDbBuilder("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview")
71+
.Build();
72+
await _container.StartAsync().ConfigureAwait(false);
7873

79-
return (endpoint, container);
74+
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
75+
{
76+
try
77+
{
78+
_container.DisposeAsync().AsTask().GetAwaiter().GetResult();
79+
}
80+
catch
81+
{
82+
// Best-effort cleanup: container may already be stopped or Docker daemon
83+
// may have exited before the process exit handler runs.
84+
}
85+
};
86+
87+
DefaultConnection = new UriBuilder(
88+
Uri.UriSchemeHttp,
89+
_container.Hostname,
90+
_container.GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)).ToString();
91+
HttpMessageHandler = _container.HttpMessageHandler;
92+
}
93+
catch when (!SkipConnectionCheck)
94+
{
95+
// Any failure (Docker not installed, daemon not running, image pull failure, etc.)
96+
// falls back to the default endpoint. The connection check in CosmosTestStore will
97+
// determine whether the emulator is actually reachable and skip tests if not.
98+
DefaultConnection = "https://localhost:8081";
99+
}
100+
101+
_initialized = true;
80102
}
81-
catch
103+
finally
82104
{
83-
// Any failure (Docker not installed, daemon not running, image pull failure, etc.)
84-
// falls back to the default endpoint. The connection check in CosmosTestStore will
85-
// determine whether the emulator is actually reachable and skip tests if not.
86-
return ("https://localhost:8081", null);
105+
_initSemaphore.Release();
87106
}
88107
}
89108

90-
private static bool TryProbeEmulator(string endpoint)
109+
private static async Task<bool> TryProbeEmulatorAsync(string endpoint)
91110
{
92111
try
93112
{
@@ -97,7 +116,7 @@ private static bool TryProbeEmulator(string endpoint)
97116
};
98117
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(3) };
99118
// Any successful response (even 401) means the emulator is up and accepting connections.
100-
using var response = client.GetAsync(endpoint).GetAwaiter().GetResult();
119+
using var response = await client.GetAsync(endpoint).ConfigureAwait(false);
101120
return true;
102121
}
103122
catch
@@ -131,7 +150,7 @@ private static bool TryProbeEmulator(string endpoint)
131150

132151
public static bool SkipConnectionCheck { get; } = string.Equals(Config["SkipConnectionCheck"], "true", StringComparison.OrdinalIgnoreCase);
133152

134-
public static string EmulatorType => IsTestContainer
153+
public static string EmulatorType => _container != null
135154
? "linux"
136155
: Config["EmulatorType"] ?? (!OperatingSystem.IsWindows() ? "linux" : "");
137156

0 commit comments

Comments
 (0)