From 71dbd6bdd2c94e9db9fc71c78f5eda4ef873c75f Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Mon, 18 Nov 2024 16:41:05 +0100 Subject: [PATCH 1/3] Update documentation SynchronizationContext in FakeTimeProvider --- .../README.md | 45 ++++++++----------- .../FakeTimeProviderTests.cs | 4 +- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md index c1dfddbb9f6..0e5bd7d25bd 100644 --- a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md @@ -42,18 +42,18 @@ timeProvider.Advance(TimeSpan.FromSeconds(5)); myComponent.CheckState(); ``` -## Use ConfigureAwait(true) with FakeTimeProvider.Advance +## SynchronizationContext in Tests -The Advance method is used to simulate the passage of time. This can be useful in tests where you need to control the timing of asynchronous operations. -When awaiting a task in a test that uses `FakeTimeProvider`, it's important to use `ConfigureAwait(true)`. +### xUnit v2 -Here's an example: +Some testing libraries such as xUnit v2 provide custom `SynchronizationContext` for running tests. xUnit v2, for instance, provides `AsyncTestSyncContext` that allows to properly manage asynchronous operations withing the test execution. However, it brings an issue when we test asynchronous code that uses `ConfigureAwait(false)` in combination with class like `FakeTimeProvider`. In such cases, the xUnit context may lose track of the continuation, causing the test to hang unexpectedly, whether the test itself is asynchronous or not. -```cs -await provider.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(true); +To prevent this issue, remove the xUnit context for tests dependent on `FakeTimeProvider` by setting the synchronization context to `null`: +``` +SynchronizationContext.SetSynchronizationContext(null) ``` -This ensures that the continuation of the awaited task (i.e., the code that comes after the await statement) runs in the original context. +The `Advance` method is used to simulate the passage of time. Below is an example how to create a test for a code that uses `ConfigureAwait(false)` that ensures that the continuation of the awaited task (i.e., the code that comes after the await statement) works correctly. For a more realistic example, consider the following test using Polly: @@ -79,35 +79,21 @@ public class SomeService(TimeProvider timeProvider) public async Task PollyRetry(double taskDelay, double cancellationSeconds) { - CancellationTokenSource cts = new(TimeSpan.FromSeconds(cancellationSeconds), timeProvider); Tries = 0; - - // get a context from the pool and return it when done - var context = ResilienceContextPool.Shared.Get( - // ensure execution continues on captured context - continueOnCapturedContext: true, - cancellationToken: cts.Token); - - var result = await _retryPipeline.ExecuteAsync( + return await _retryPipeline.ExecuteAsync( async _ => { Tries++; - // Simulate a task that takes some time to complete - await Task.Delay(TimeSpan.FromSeconds(taskDelay), timeProvider).ConfigureAwait(true); - - if (Tries <= 2) + // With xUnit Context this would fail. + await timeProvider.Delay(TimeSpan.FromSeconds(taskDelay)).ConfigureAwait(false); + if (Tries < 2) { throw new InvalidOperationException(); } - return Tries; }, - context); - - ResilienceContextPool.Shared.Return(context); - - return result; + CancellationToken.None); } } @@ -118,6 +104,9 @@ public class SomeServiceTests [Fact] public void PollyRetry_ShouldHave2Tries() { + // Arrange + // Remove xUnit Context for this test + SynchronizationContext.SetSynchronizationContext(null); var timeProvider = new FakeTimeProvider(); var someService = new SomeService(timeProvider); @@ -138,6 +127,10 @@ public class SomeServiceTests } ``` +### xUnit v3 + +`AsyncTestSyncContext` has been removed more [here](https://xunit.net/docs/getting-started/v3/migration) so described issue is no longer a problem. + ## Feedback & Contributing We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions). diff --git a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs index 4f29e960975..6daaf635c06 100644 --- a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Time.Testing; using Xunit; namespace Microsoft.Extensions.Time.Testing.Test; @@ -441,6 +440,7 @@ public void ShouldResetGateUnderLock_PreventingContextSwitching_AffectionOnTimer [Fact] public void SimulateRetryPolicy() { + SynchronizationContext.SetSynchronizationContext(null); // Arrange var retries = 42; var tries = 0; @@ -469,7 +469,7 @@ async Task simulatedPollyRetry() catch (InvalidOperationException) { // ConfigureAwait(true) is required to ensure that tasks continue on the captured context - await provider.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(true); + await provider.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(false); } } } From a63761a68a9d7e95b15a109271aae42ec64a3166 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Tue, 19 Nov 2024 12:26:43 +0100 Subject: [PATCH 2/3] Fix lint error --- .../FakeTimeProviderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs index 6daaf635c06..58e218647b4 100644 --- a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs @@ -440,8 +440,8 @@ public void ShouldResetGateUnderLock_PreventingContextSwitching_AffectionOnTimer [Fact] public void SimulateRetryPolicy() { - SynchronizationContext.SetSynchronizationContext(null); // Arrange + SynchronizationContext.SetSynchronizationContext(null); var retries = 42; var tries = 0; var taskDelay = 0.5; From 5975cbf3b4ee4723fec21a743b033ab7bbf4372e Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Tue, 19 Nov 2024 13:15:40 +0100 Subject: [PATCH 3/3] Update documentation --- .../Microsoft.Extensions.TimeProvider.Testing/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md index 0e5bd7d25bd..f8faa6fdf2e 100644 --- a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md @@ -42,11 +42,11 @@ timeProvider.Advance(TimeSpan.FromSeconds(5)); myComponent.CheckState(); ``` -## SynchronizationContext in Tests +## SynchronizationContext in xUnit Tests ### xUnit v2 -Some testing libraries such as xUnit v2 provide custom `SynchronizationContext` for running tests. xUnit v2, for instance, provides `AsyncTestSyncContext` that allows to properly manage asynchronous operations withing the test execution. However, it brings an issue when we test asynchronous code that uses `ConfigureAwait(false)` in combination with class like `FakeTimeProvider`. In such cases, the xUnit context may lose track of the continuation, causing the test to hang unexpectedly, whether the test itself is asynchronous or not. +Some testing libraries such as xUnit v2 provide custom `SynchronizationContext` for running tests. xUnit v2, for instance, provides `AsyncTestSyncContext` that allows to properly manage asynchronous operations withing the test execution. However, it brings an issue when we test asynchronous code that uses `ConfigureAwait(false)` in combination with class like `FakeTimeProvider`. In such cases, the xUnit context may lose track of the continuation, causing the test to become unresponsive, whether the test itself is asynchronous or not. To prevent this issue, remove the xUnit context for tests dependent on `FakeTimeProvider` by setting the synchronization context to `null`: ```