diff --git a/azure-pipelines-internal-tests.yml b/azure-pipelines-internal-tests.yml
index ae6a00acb79..bb21351c0c5 100644
--- a/azure-pipelines-internal-tests.yml
+++ b/azure-pipelines-internal-tests.yml
@@ -137,7 +137,7 @@ extends:
Test__Cosmos__DefaultConnection: $(_CosmosConnectionUrl)
Test__SqlServer__DefaultConnection: "Data Source="
displayName: Build
- - script: .dotnet\dotnet test test\EFCore.ApiBaseline.Tests -configuration $(_BuildConfig)
+ - script: .dotnet\dotnet test test\EFCore.ApiBaseline.Tests -p:Configuration=$(_BuildConfig)
displayName: Test API Baselines
condition: succeeded()
- powershell: |
diff --git a/azure-pipelines-public.yml b/azure-pipelines-public.yml
index ced0274d3b9..87a1cf8e229 100644
--- a/azure-pipelines-public.yml
+++ b/azure-pipelines-public.yml
@@ -81,7 +81,7 @@ stages:
Test__Cosmos__DefaultConnection: $(_CosmosConnectionUrl)
Test__SqlServer__DefaultConnection: "Data Source="
name: Build
- - script: .dotnet\dotnet test test\EFCore.ApiBaseline.Tests -configuration $(_BuildConfig)
+ - script: .dotnet\dotnet test test\EFCore.ApiBaseline.Tests -p:Configuration=$(_BuildConfig)
displayName: Test API Baselines
condition: succeeded()
- powershell: |
diff --git a/eng/helix.proj b/eng/helix.proj
index d68ccd054e0..3dfca72e27d 100644
--- a/eng/helix.proj
+++ b/eng/helix.proj
@@ -125,15 +125,18 @@
such as [ConditionalClass]/[ConditionalAssembly] add `category=failing` to tests whose
condition evaluates to false; this filter is what actually skips them at the runner.
+ The 'report-trx' argument is required so each Helix work item produces a TRX file that Arcade's
+ Helix reporter uploads to Azure DevOps.
+
This must run AFTER any per-queue ItemGroup that re-Includes MTPProject items.
-->
- $(XUnitV3Arguments) --filter-not-trait category=failing --ignore-exit-code 8
+ --report-trx --filter-not-trait category=failing --ignore-exit-code 8
- 03:00:00
+ 03:00:00
diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
index 6a5057aa888..acb1374b4e7 100644
--- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
@@ -317,6 +317,10 @@ public override Task CleanAsync(DbContext context, bool createTables = true)
private async Task CleanAsyncImpl(DbContext context, bool createTables)
{
var created = await EnsureCreatedAsync(context).ConfigureAwait(false);
+
+ // Containers are deleted and recreated below only when the database already existed. A freshly-created
+ // database has brand-new containers whose metadata cannot be stale.
+ var containersRecreated = !created;
try
{
if (!created)
@@ -334,12 +338,23 @@ private async Task CleanAsyncImpl(DbContext context, bool createTables)
created = await context.Database.EnsureCreatedAsync().ConfigureAwait(false);
if (!created)
{
+ if (containersRecreated)
+ {
+ await RefreshContainerMetadataAsync(context).ConfigureAwait(false);
+ }
+
await SeedAsync(context).ConfigureAwait(false);
}
}
else
{
await CreateContainersAsync(context).ConfigureAwait(false);
+
+ if (containersRecreated)
+ {
+ await RefreshContainerMetadataAsync(context).ConfigureAwait(false);
+ }
+
await SeedAsync(context).ConfigureAwait(false);
}
}
@@ -357,6 +372,63 @@ private async Task CleanAsyncImpl(DbContext context, bool createTables)
}
}
+ // Deleting and recreating a container gives it a new resource id (_rid), but the shared CosmosClient caches each
+ // container's _rid (and the partition key ranges keyed by it) by container name. The first operation on the
+ // recreated container - whether it is the reseed below or the next test's first query - can then fail with
+ // "NotFound (404) ... GetTargetPartitionKeyRanges ... failed due to stale cache", and the query pipeline does not
+ // transparently refresh and retry. Prime the client's caches here, right after recreating the containers and before
+ // any test touches them. Each attempt re-reads the container by name (refreshing the collection cache with the new
+ // _rid) and then runs a query - the exact path that otherwise fails. Because the recreate can take a moment to
+ // become consistent on the emulator gateway, retry with a short delay until a query succeeds. This is the single
+ // place that handles the stale-metadata problem; it is fully best-effort and must never fail the clean, so all
+ // errors are ultimately swallowed.
+ private async Task RefreshContainerMetadataAsync(DbContext context)
+ {
+ const int maxAttempts = 10;
+
+ try
+ {
+ var cosmosClient = context.Database.GetCosmosClient();
+ var database = cosmosClient.GetDatabase(Name);
+ var model = context.GetService().Model;
+ var countQuery = new QueryDefinition("SELECT VALUE COUNT(1) FROM c");
+
+ foreach (var containerProperties in GetContainersToCreate(model))
+ {
+ var container = database.GetContainer(containerProperties.Id);
+
+ for (var attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ try
+ {
+ await container.ReadContainerAsync().ConfigureAwait(false);
+
+ using var iterator = container.GetItemQueryIterator(countQuery);
+ while (iterator.HasMoreResults)
+ {
+ await iterator.ReadNextAsync().ConfigureAwait(false);
+ }
+
+ break;
+ }
+ catch when (attempt < maxAttempts)
+ {
+ // The recreate has not become consistent yet; wait for the gateway to catch up and retry.
+ await Task.Delay(200).ConfigureAwait(false);
+ }
+ catch
+ {
+ // Best-effort priming; never let it fail the clean.
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Never let cache priming fail the clean.
+ }
+ }
+
private async Task CreateContainersAsync(DbContext context)
{
var databaseAccount = await GetDBAccount().ConfigureAwait(false);
diff --git a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/ProxyGraphUpdatesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/ProxyGraphUpdatesInMemoryTest.cs
index a4c6b7c47f9..682504b6b49 100644
--- a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/ProxyGraphUpdatesInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/ProxyGraphUpdatesInMemoryTest.cs
@@ -10,12 +10,10 @@ public abstract class ProxyGraphUpdatesInMemoryTestBase(TFixture fixtu
where TFixture : ProxyGraphUpdatesInMemoryTestBase.ProxyGraphUpdatesInMemoryFixtureBase, new()
{
// FK constraint checking.
- [Fact]
public override Task Optional_one_to_one_relationships_are_one_to_one()
=> Assert.ThrowsAnyAsync(() => base.Optional_one_to_one_relationships_are_one_to_one());
// FK constraint checking.
- [Fact]
public override Task Optional_one_to_one_with_AK_relationships_are_one_to_one()
=> Assert.ThrowsAnyAsync(() => base.Optional_one_to_one_with_AK_relationships_are_one_to_one());
@@ -32,6 +30,12 @@ public override Task Optional_one_to_one_are_orphaned(
CascadeTiming deleteOrphansTiming)
=> Task.CompletedTask;
+ // Cascade delete.
+ public override Task Optional_one_to_one_with_alternate_key_are_orphaned(
+ CascadeTiming cascadeDeleteTiming,
+ CascadeTiming deleteOrphansTiming)
+ => Task.CompletedTask;
+
// Cascade delete.
public override Task Optional_many_to_one_dependents_are_orphaned_in_store(
CascadeTiming cascadeDeleteTiming,
@@ -94,12 +98,10 @@ public override Task Required_many_to_one_dependents_with_alternate_key_are_casc
=> Task.CompletedTask;
// FK constraint checking.
- [Fact]
public override Task Required_one_to_one_relationships_are_one_to_one()
=> Assert.ThrowsAnyAsync(() => base.Required_one_to_one_relationships_are_one_to_one());
// FK constraint checking.
- [Fact]
public override Task Required_one_to_one_with_AK_relationships_are_one_to_one()
=> Assert.ThrowsAnyAsync(() => base.Required_one_to_one_with_AK_relationships_are_one_to_one());
@@ -115,6 +117,14 @@ public override Task Required_many_to_one_dependents_with_alternate_key_are_casc
public override Task Can_attach_full_optional_graph_of_duplicates()
=> Task.CompletedTask;
+ // Graph fixup ordering is non-deterministic on InMemory.
+ public override Task Can_attach_full_required_AK_graph_of_duplicates()
+ => Task.CompletedTask;
+
+ // Graph fixup ordering is non-deterministic on InMemory.
+ public override Task No_fixup_to_Deleted_entities()
+ => Task.CompletedTask;
+
// Cascade delete.
public override Task Required_non_PK_one_to_one_with_alternate_key_are_cascade_deleted(
CascadeTiming cascadeDeleteTiming,
@@ -127,6 +137,10 @@ public override Task Reparent_required_non_PK_one_to_one_with_alternate_key(
bool useExistingRoot)
=> Task.CompletedTask;
+ // Cascade delete.
+ public override Task Save_changed_optional_one_to_one_with_alternate_key_in_store()
+ => Task.CompletedTask;
+
// Cascade delete.
public override Task Sever_required_one_to_one(ChangeMechanism changeMechanism)
=> Task.CompletedTask;
@@ -157,10 +171,17 @@ protected override async Task ExecuteWithStrategyInTransactionAsync(
Func nestedTestOperation2 = null,
Func nestedTestOperation3 = null)
{
- await base.ExecuteWithStrategyInTransactionAsync(
- testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3);
-
- await Fixture.ReseedAsync();
+ // InMemory has no real transactions, so the shared store is mutated directly by each test and must be
+ // reseeded afterwards.
+ try
+ {
+ await base.ExecuteWithStrategyInTransactionAsync(
+ testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3);
+ }
+ finally
+ {
+ await Fixture.ReseedAsync();
+ }
}
public abstract class ProxyGraphUpdatesInMemoryFixtureBase : ProxyGraphUpdatesFixtureBase
diff --git a/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs b/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs
index 184aa8c5f3e..b3886a52788 100644
--- a/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs
@@ -336,41 +336,84 @@ public virtual async Task SaveChanges_uses_ambient_transaction_with_connectionSt
return;
}
- DbConnection connection;
- using (var context = CreateContextWithConnectionString())
+ DbConnection connection = null;
+
+ await RetryOnDistributedTransactionNotSupportedAsync(async () =>
{
- using (TestUtilities.TestStore.CreateTransactionScope())
+ using (var context = CreateContextWithConnectionString())
{
- context.Database.AutoTransactionBehavior = autoTransactionBehavior;
+ using (TestUtilities.TestStore.CreateTransactionScope())
+ {
+ context.Database.AutoTransactionBehavior = autoTransactionBehavior;
- connection = context.Database.GetDbConnection();
- Assert.Equal(ConnectionState.Closed, connection.State);
+ connection = context.Database.GetDbConnection();
+ Assert.Equal(ConnectionState.Closed, connection.State);
- await context.AddAsync(
- new TransactionCustomer { Id = 77, Name = "Bobble" });
+ await context.AddAsync(
+ new TransactionCustomer { Id = 77, Name = "Bobble" });
- context.Entry(context.Set().OrderBy(c => c.Id).Last()).State = EntityState.Added;
+ context.Entry(context.Set().OrderBy(c => c.Id).Last()).State = EntityState.Added;
- if (async)
- {
- await Assert.ThrowsAsync(() => context.SaveChangesAsync());
- }
- else
- {
- Assert.Throws(() => context.SaveChanges());
- }
+ // SaveChanges is expected to fail with DbUpdateException (duplicate key).
+ try
+ {
+ if (async)
+ {
+ await context.SaveChangesAsync();
+ }
+ else
+ {
+ context.SaveChanges();
+ }
+
+ Assert.Fail("Expected a DbUpdateException to be thrown.");
+ }
+ catch (PlatformNotSupportedException)
+ {
+ // When the ambient transaction is promoted to a distributed one and throws PlatformNotSupportedException, let it
+ // propagate so the whole operation is retried.
+ throw;
+ }
+ catch (DbUpdateException)
+ {
+ }
- Assert.Equal(ConnectionState.Closed, connection.State);
+ Assert.Equal(ConnectionState.Closed, connection.State);
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded;
+ context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded;
+ }
}
- }
+ });
Assert.Equal(ConnectionState.Closed, connection.State);
AssertStoreInitialState();
}
+ // Sometimes an ambient/enlisted transaction ends up spanning more than one physical connection due to concurrency issues
+ // and is promoted to a distributed transaction, which throws PlatformNotSupportedException.
+ // Retry the operation a few times (with exponential back-off) so a run that keeps to a single connection (and
+ // therefore reaches the behavior under test) is used.
+ private static async Task RetryOnDistributedTransactionNotSupportedAsync(Func test)
+ {
+ const int maxAttempts = 3;
+ var delay = TimeSpan.FromSeconds(1);
+
+ for (var attempt = 1; ; attempt++)
+ {
+ try
+ {
+ await test();
+ return;
+ }
+ catch (PlatformNotSupportedException) when (attempt < maxAttempts)
+ {
+ await Task.Delay(delay);
+ delay *= 2;
+ }
+ }
+ }
+
[Theory, InlineData(true), InlineData(false)]
public virtual void SaveChanges_throws_for_suppressed_ambient_transactions(bool connectionString)
{
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs
index 00fa3990317..bb5e873c2cb 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs
@@ -249,14 +249,16 @@ public virtual Task Average_over_nested_subquery(bool async)
=> AssertAverage(
async,
ss => ss.Set().OrderBy(c => c.CustomerID).Take(3),
- selector: c => (decimal)c.Orders.Average(double (o) => 5 + o.OrderDetails.Average(int (od) => od.ProductID)));
+ selector: c => (decimal)c.Orders.Average(double (o) => 5 + o.OrderDetails.Average(int (od) => od.ProductID)),
+ asserter: (e, a) => Assert.Equal(e, a, 10));
[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task Average_over_max_subquery(bool async)
=> AssertAverage(
async,
ss => ss.Set().OrderBy(c => c.CustomerID).Take(3),
- selector: c => (decimal)c.Orders.Average(int (o) => 5 + o.OrderDetails.Max(int (od) => od.ProductID)));
+ selector: c => (decimal)c.Orders.Average(int (o) => 5 + o.OrderDetails.Max(int (od) => od.ProductID)),
+ asserter: (e, a) => Assert.Equal(e, a, 10));
[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task Average_on_float_column(bool async)