Skip to content

Commit 45dc35a

Browse files
authored
Merge pull request #26 from EFNext/fix/cosmos-tests
Update Cosmos tests to handle unsupported scenarios with inconclusive assertions
2 parents 011c931 + d871a95 commit 45dc35a

6 files changed

Lines changed: 245 additions & 34 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
with:
6262
name: test-results
6363
path: |
64-
./test-results/**/*.trx
64+
./test-results/*.trx
6565
./test-results/**/coverage.xml
6666
retention-days: 14
6767

@@ -78,7 +78,7 @@ jobs:
7878
uses: dorny/test-reporter@v1
7979
with:
8080
name: Test Results
81-
path: ./test-results/**/*.trx
81+
path: ./test-results/*.trx
8282
reporter: dotnet-trx
8383

8484
- name: Pack
@@ -146,26 +146,44 @@ jobs:
146146
-p:TestDatabase=${{ matrix.database }}
147147
148148
- name: Test
149-
run: >-
150-
dotnet test --no-build -c Release
151-
--project tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests
152-
-p:TestDatabase=${{ matrix.database }}
153-
--
154-
--report-trx --report-trx-filename results.trx
155-
--results-directory ./test-results
149+
run: |
150+
if [ "${{ matrix.database }}" = "Cosmos" ]; then
151+
# Cosmos vNext emulator binds host port 8081 (the .NET SDK
152+
# connects to the gateway-advertised endpoint, which must
153+
# match the host-side port), so only one emulator can run
154+
# at a time. Run TFMs sequentially.
155+
for tfm in net8.0 net9.0 net10.0; do
156+
echo "::group::Cosmos tests ($tfm)"
157+
dotnet test --no-build -c Release \
158+
--project tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests \
159+
-p:TestDatabase=${{ matrix.database }} \
160+
-f "$tfm" \
161+
--results-directory ./test-results \
162+
-- \
163+
--report-trx --report-trx-filename "results-$tfm.trx"
164+
echo "::endgroup::"
165+
done
166+
else
167+
dotnet test --no-build -c Release \
168+
--project tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests \
169+
-p:TestDatabase=${{ matrix.database }} \
170+
--results-directory ./test-results \
171+
-- \
172+
--report-trx --report-trx-filename results.trx
173+
fi
156174
157175
- name: Upload test results
158176
if: always()
159177
uses: actions/upload-artifact@v4
160178
with:
161179
name: container-test-results-${{ matrix.database }}
162-
path: ./test-results/**/*.trx
180+
path: ./test-results/*.trx
163181
retention-days: 14
164182

165183
- name: Test report
166184
if: always()
167185
uses: dorny/test-reporter@v1
168186
with:
169187
name: Container Test Results (${{ matrix.database }})
170-
path: ./test-results/**/*.trx
188+
path: ./test-results/*.trx
171189
reporter: dotnet-trx

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.23.60" />
2121
<PackageVersion Include="Basic.Reference.Assemblies.Net90" Version="1.8.4" />
2222
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
23+
<PackageVersion Include="Testcontainers" Version="4.3.0" />
2324
<PackageVersion Include="Testcontainers.MsSql" Version="4.3.0" />
2425
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.3.0" />
25-
<PackageVersion Include="Testcontainers.CosmosDb" Version="4.3.0" />
2626
<PackageVersion Include="Testcontainers.MySql" Version="4.3.0" />
2727
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="8.0.25" />
2828
</ItemGroup>

tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@
6565
</ItemGroup>
6666

6767
<!-- Cosmos DB (conditional) -->
68+
<!-- Uses the Linux vNext preview emulator via the generic Testcontainers
69+
package; Testcontainers.CosmosDb only wraps the classic emulator. -->
6870
<ItemGroup Condition="'$(TestDatabase)' == 'Cosmos' or '$(TestDatabase)' == 'All'">
69-
<PackageReference Include="Testcontainers.CosmosDb" />
71+
<PackageReference Include="Testcontainers" />
7072
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos"
7173
Condition="'$(TargetFramework)' == 'net8.0'" />
7274
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos"

tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Infrastructure/CommonScenarioTestBase.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public async Task Select_Total_ReturnsCorrectValues()
3131
}
3232

3333
[TestMethod]
34-
public async Task Where_TotalGreaterThan100_FiltersCorrectly()
34+
public virtual async Task Where_TotalGreaterThan100_FiltersCorrectly()
3535
{
3636
Expression<Func<Order, double>> totalExpr = o => o.Total;
3737
var expanded = (Expression<Func<Order, double>>)totalExpr.ExpandExpressives();
@@ -47,7 +47,7 @@ public async Task Where_TotalGreaterThan100_FiltersCorrectly()
4747
}
4848

4949
[TestMethod]
50-
public async Task Where_NoMatch_ReturnsEmpty()
50+
public virtual async Task Where_NoMatch_ReturnsEmpty()
5151
{
5252
Expression<Func<Order, double>> totalExpr = o => o.Total;
5353
var expanded = (Expression<Func<Order, double>>)totalExpr.ExpandExpressives();
@@ -62,7 +62,7 @@ public async Task Where_NoMatch_ReturnsEmpty()
6262
}
6363

6464
[TestMethod]
65-
public async Task OrderByDescending_Total_ReturnsSortedDescending()
65+
public virtual async Task OrderByDescending_Total_ReturnsSortedDescending()
6666
{
6767
Expression<Func<Order, double>> totalExpr = o => o.Total;
6868
var expanded = (Expression<Func<Order, double>>)totalExpr.ExpandExpressives();
@@ -280,7 +280,7 @@ public async Task Select_InstanceProperty_ViaExpressiveFor_ReturnsConstant()
280280
// ── Loop Tests ──────────────────────────────────────────────────────────
281281

282282
[TestMethod]
283-
public async Task Select_ItemCount_ReturnsCorrectCounts()
283+
public virtual async Task Select_ItemCount_ReturnsCorrectCounts()
284284
{
285285
Expression<Func<Order, int>> expr = o => o.ItemCount();
286286
var expanded = (Expression<Func<Order, int>>)expr.ExpandExpressives();
@@ -291,7 +291,7 @@ public async Task Select_ItemCount_ReturnsCorrectCounts()
291291
}
292292

293293
[TestMethod]
294-
public async Task Select_ItemTotal_ReturnsCorrectTotals()
294+
public virtual async Task Select_ItemTotal_ReturnsCorrectTotals()
295295
{
296296
Expression<Func<Order, double>> expr = o => o.ItemTotal();
297297
var expanded = (Expression<Func<Order, double>>)expr.ExpandExpressives();
@@ -302,7 +302,7 @@ public async Task Select_ItemTotal_ReturnsCorrectTotals()
302302
}
303303

304304
[TestMethod]
305-
public async Task Select_HasExpensiveItems_ReturnsCorrectFlags()
305+
public virtual async Task Select_HasExpensiveItems_ReturnsCorrectFlags()
306306
{
307307
Expression<Func<Order, bool>> expr = o => o.HasExpensiveItems();
308308
var expanded = (Expression<Func<Order, bool>>)expr.ExpandExpressives();
@@ -313,7 +313,7 @@ public async Task Select_HasExpensiveItems_ReturnsCorrectFlags()
313313
}
314314

315315
[TestMethod]
316-
public async Task Select_AllItemsAffordable_ReturnsCorrectFlags()
316+
public virtual async Task Select_AllItemsAffordable_ReturnsCorrectFlags()
317317
{
318318
Expression<Func<Order, bool>> expr = o => o.AllItemsAffordable();
319319
var expanded = (Expression<Func<Order, bool>>)expr.ExpandExpressives();
@@ -324,7 +324,7 @@ public async Task Select_AllItemsAffordable_ReturnsCorrectFlags()
324324
}
325325

326326
[TestMethod]
327-
public async Task Select_ItemTotalForExpensive_ReturnsCorrectTotals()
327+
public virtual async Task Select_ItemTotalForExpensive_ReturnsCorrectTotals()
328328
{
329329
Expression<Func<Order, double>> expr = o => o.ItemTotalForExpensive();
330330
var expanded = (Expression<Func<Order, double>>)expr.ExpandExpressives();
@@ -337,7 +337,7 @@ public async Task Select_ItemTotalForExpensive_ReturnsCorrectTotals()
337337
// ── Null Conditional ────────────────────────────────────────────────────
338338

339339
[TestMethod]
340-
public async Task Select_CustomerName_ReturnsCorrectNullableValues()
340+
public virtual async Task Select_CustomerName_ReturnsCorrectNullableValues()
341341
{
342342
Expression<Func<Order, string?>> expr = o => o.CustomerName;
343343
var expanded = (Expression<Func<Order, string?>>)expr.ExpandExpressives();
@@ -348,7 +348,7 @@ public async Task Select_CustomerName_ReturnsCorrectNullableValues()
348348
}
349349

350350
[TestMethod]
351-
public async Task Select_TagLength_ReturnsCorrectNullableValues()
351+
public virtual async Task Select_TagLength_ReturnsCorrectNullableValues()
352352
{
353353
Expression<Func<Order, int?>> expr = o => o.TagLength;
354354
var expanded = (Expression<Func<Order, int?>>)expr.ExpandExpressives();
@@ -375,7 +375,7 @@ public async Task Where_CustomerNameEquals_FiltersCorrectly()
375375
}
376376

377377
[TestMethod]
378-
public async Task Where_CustomerNameIsNull_FiltersCorrectly()
378+
public virtual async Task Where_CustomerNameIsNull_FiltersCorrectly()
379379
{
380380
Expression<Func<Order, string?>> nameExpr = o => o.CustomerName;
381381
var expanded = (Expression<Func<Order, string?>>)nameExpr.ExpandExpressives();
@@ -411,7 +411,7 @@ public virtual async Task OrderBy_TagLength_NullsAppearFirst()
411411
// ── Nullable Chain ──────────────────────────────────────────────────────
412412

413413
[TestMethod]
414-
public async Task Select_CustomerCountry_TwoLevelChain()
414+
public virtual async Task Select_CustomerCountry_TwoLevelChain()
415415
{
416416
Expression<Func<Order, string?>> expr = o => o.CustomerCountry;
417417
var expanded = (Expression<Func<Order, string?>>)expr.ExpandExpressives();
@@ -526,7 +526,7 @@ public async Task Polyfill_Arithmetic_ProjectsCorrectly()
526526
}
527527

528528
[TestMethod]
529-
public async Task Polyfill_NullConditional_ProjectsCorrectly()
529+
public virtual async Task Polyfill_NullConditional_ProjectsCorrectly()
530530
{
531531
var expr = ExpressionPolyfill.Create((Order o) => o.Customer != null ? o.Customer.Name : null);
532532

@@ -607,7 +607,7 @@ public async Task Select_FormattedPrice_UsesToStringWithFormat()
607607
}
608608

609609
[TestMethod]
610-
public async Task Where_Summary_TranslatesToSql()
610+
public virtual async Task Where_Summary_TranslatesToSql()
611611
{
612612
// Summary uses string.Concat(string, string, string, string).
613613
// This verifies the 4-arg overload translates to SQL (Where throws if not).
@@ -621,7 +621,7 @@ public async Task Where_Summary_TranslatesToSql()
621621
}
622622

623623
[TestMethod]
624-
public async Task Where_DetailedSummary_ConcatArrayTranslatesToSql()
624+
public virtual async Task Where_DetailedSummary_ConcatArrayTranslatesToSql()
625625
{
626626
// DetailedSummary has 7 string parts, so the emitter produces string.Concat(string[]).
627627
// FlattenConcatArrayCalls rewrites it to chained Concat calls for EF Core.
@@ -649,7 +649,7 @@ public async Task Select_GetGrade_ReturnsCorrectValues()
649649
}
650650

651651
[TestMethod]
652-
public async Task OrderBy_GetGrade_ReturnsSorted()
652+
public virtual async Task OrderBy_GetGrade_ReturnsSorted()
653653
{
654654
Expression<Func<Order, string>> gradeExpr = o => o.GetGrade();
655655
var expandedGrade = (Expression<Func<Order, string>>)gradeExpr.ExpandExpressives();
@@ -664,7 +664,7 @@ public async Task OrderBy_GetGrade_ReturnsSorted()
664664
}
665665

666666
[TestMethod]
667-
public async Task OrderByDescending_GetGrade_ReturnsSortedDescending()
667+
public virtual async Task OrderByDescending_GetGrade_ReturnsSortedDescending()
668668
{
669669
Expression<Func<Order, string>> gradeExpr = o => o.GetGrade();
670670
var expandedGrade = (Expression<Func<Order, string>>)gradeExpr.ExpandExpressives();

tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Infrastructure/ContainerFixture.cs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
using Testcontainers.PostgreSql;
88
#endif
99
#if TEST_COSMOS
10-
using Testcontainers.CosmosDb;
10+
using System.Net.Http;
11+
using DotNet.Testcontainers.Builders;
12+
using DotNet.Testcontainers.Containers;
1113
#endif
1214
#if TEST_POMELO_MYSQL && !NET10_0_OR_GREATER
1315
using Testcontainers.MySql;
@@ -32,7 +34,18 @@ public static class ContainerFixture
3234
#endif
3335

3436
#if TEST_COSMOS
35-
private static CosmosDbContainer? _cosmos;
37+
// Cosmos DB Linux emulator vNext (preview). Microsoft's officially
38+
// recommended image for CI/CD use; the classic emulator is documented
39+
// as not running reliably on hosted CI agents.
40+
// https://learn.microsoft.com/en-us/azure/cosmos-db/emulator-linux
41+
private const string CosmosImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview";
42+
private const int CosmosGatewayPort = 8081;
43+
44+
// Well-known emulator key (same value used by classic and vNext).
45+
private const string CosmosEmulatorKey =
46+
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
47+
48+
private static IContainer? _cosmos;
3649
public static string? CosmosConnectionString { get; private set; }
3750
#endif
3851

@@ -63,7 +76,20 @@ public static async Task InitializeAsync(TestContext _)
6376
tasks.Add(StartPostgresAsync());
6477
#endif
6578
#if TEST_COSMOS
66-
_cosmos = new CosmosDbBuilder().Build();
79+
// The .NET Cosmos SDK reads the gateway's account metadata, which
80+
// returns the emulator's *internal* endpoint (localhost:8081). The
81+
// SDK then connects to that endpoint from the host, so we must bind
82+
// the host port to the same number 8081 as the container port. That
83+
// means only one emulator can run per host — Cosmos tests for the
84+
// multi-TFM matrix must be serialized in CI.
85+
_cosmos = new ContainerBuilder()
86+
.WithImage(CosmosImage)
87+
// .NET SDK does not support HTTP mode against the emulator.
88+
.WithEnvironment("PROTOCOL", "https")
89+
.WithPortBinding(CosmosGatewayPort, CosmosGatewayPort)
90+
.WithWaitStrategy(Wait.ForUnixContainer()
91+
.UntilPortIsAvailable(CosmosGatewayPort))
92+
.Build();
6793
tasks.Add(StartCosmosAsync());
6894
#endif
6995
#if TEST_POMELO_MYSQL && !NET10_0_OR_GREATER
@@ -132,7 +158,40 @@ private static async Task StartPostgresAsync()
132158
private static async Task StartCosmosAsync()
133159
{
134160
await _cosmos!.StartAsync();
135-
CosmosConnectionString = _cosmos.GetConnectionString();
161+
CosmosConnectionString =
162+
$"AccountEndpoint=https://localhost:{CosmosGatewayPort}/;AccountKey={CosmosEmulatorKey}";
163+
164+
// The vNext emulator binds port 8081 long before its backing
165+
// PostgreSQL + Rust gateway can serve requests. Poll the gateway
166+
// root endpoint until it returns a 200 — bypass the self-signed
167+
// cert manually since the built-in HTTP wait strategy does not.
168+
using var httpClient = new HttpClient(new HttpClientHandler
169+
{
170+
ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
171+
})
172+
{
173+
Timeout = TimeSpan.FromSeconds(5),
174+
};
175+
176+
var url = $"https://localhost:{CosmosGatewayPort}/";
177+
var deadline = DateTime.UtcNow.AddMinutes(5);
178+
while (DateTime.UtcNow < deadline)
179+
{
180+
try
181+
{
182+
var resp = await httpClient.GetAsync(url);
183+
if (resp.IsSuccessStatusCode)
184+
return;
185+
}
186+
catch
187+
{
188+
// Gateway not yet ready — keep polling.
189+
}
190+
await Task.Delay(TimeSpan.FromSeconds(1));
191+
}
192+
193+
throw new TimeoutException(
194+
$"Cosmos vNext emulator at {url} did not become ready within 5 minutes.");
136195
}
137196
#endif
138197

0 commit comments

Comments
 (0)