Skip to content

Commit 7cfaf05

Browse files
committed
Add regression test for .ConfigureAwait(false)
1 parent 864e88d commit 7cfaf05

File tree

1 file changed

+87
-2
lines changed

1 file changed

+87
-2
lines changed

src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,16 @@ private HubConnection CreateHubConnection(
6767
return hubConnectionBuilder.Build();
6868
}
6969

70-
private Func<EndPoint, ValueTask<ConnectionContext>> GetHttpConnectionFactory(string url, ILoggerFactory loggerFactory, string path, HttpTransportType transportType, TransferFormat transferFormat)
70+
private static Func<EndPoint, ValueTask<ConnectionContext>> GetHttpConnectionFactory(string url, ILoggerFactory loggerFactory, string path, HttpTransportType transportType, TransferFormat transferFormat)
7171
{
7272
return async endPoint =>
7373
{
7474
var httpEndpoint = (UriEndPoint)endPoint;
7575
var options = new HttpConnectionOptions { Url = httpEndpoint.Uri, Transports = transportType, DefaultTransferFormat = transferFormat };
7676
var connection = new HttpConnection(options, loggerFactory);
7777

78-
await connection.StartAsync();
78+
// This is used by CanBlockOnAsyncOperationsWithOneAtATimeSynchronizationContext, so the ConfigureAwait(false) is important.
79+
await connection.StartAsync().ConfigureAwait(false);
7980

8081
return connection;
8182
};
@@ -2090,6 +2091,90 @@ bool ExpectedErrors(WriteContext writeContext)
20902091
}
20912092
}
20922093

2094+
[Theory]
2095+
[MemberData(nameof(TransportTypes))]
2096+
public async Task CanBlockOnAsyncOperationsWithOneAtATimeSynchronizationContext(HttpTransportType transportType)
2097+
{
2098+
const int DefaultTimeout = Testing.TaskExtensions.DefaultTimeoutDuration;
2099+
2100+
await using var server = await StartServer<Startup>();
2101+
await using var connection = CreateHubConnection(server.Url, "/default", transportType, HubProtocols["json"], LoggerFactory);
2102+
await using var oneAtATimeSynchronizationContext = new OneAtATimeSynchronizationContext();
2103+
2104+
var originalSynchronizationContext = SynchronizationContext.Current;
2105+
SynchronizationContext.SetSynchronizationContext(oneAtATimeSynchronizationContext);
2106+
2107+
try
2108+
{
2109+
// Yield first so the rest of the test runs in the OneAtATimeSynchronizationContext.Run loop
2110+
await Task.Yield();
2111+
2112+
Assert.True(connection.StartAsync().Wait(DefaultTimeout));
2113+
2114+
var invokeTask = connection.InvokeAsync<string>(nameof(TestHub.HelloWorld));
2115+
Assert.True(invokeTask.Wait(DefaultTimeout));
2116+
Assert.Equal("Hello World!", invokeTask.Result);
2117+
2118+
Assert.True(connection.DisposeAsync().AsTask().Wait(DefaultTimeout));
2119+
}
2120+
catch (Exception ex)
2121+
{
2122+
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
2123+
throw;
2124+
}
2125+
finally
2126+
{
2127+
SynchronizationContext.SetSynchronizationContext(originalSynchronizationContext);
2128+
}
2129+
}
2130+
2131+
private class OneAtATimeSynchronizationContext : SynchronizationContext, IAsyncDisposable
2132+
{
2133+
private readonly Channel<(SendOrPostCallback, object)> _taskQueue = Channel.CreateUnbounded<(SendOrPostCallback, object)>();
2134+
private readonly Task _runTask;
2135+
private bool _disposed;
2136+
2137+
public OneAtATimeSynchronizationContext()
2138+
{
2139+
// Task.Run to avoid running with xUnit's AsyncTestSyncContext as well.
2140+
_runTask = Task.Run(Run);
2141+
}
2142+
2143+
public override void Post(SendOrPostCallback d, object state)
2144+
{
2145+
if (_disposed)
2146+
{
2147+
// There should be no other calls to Post() after dispose. If there are calls,
2148+
// the test has most likely failed with a timeout. Let the callbacks run so the
2149+
// timeout exception gets reported accurately instead of as a long-running test.
2150+
d(state);
2151+
}
2152+
2153+
_taskQueue.Writer.TryWrite((d, state));
2154+
}
2155+
2156+
public ValueTask DisposeAsync()
2157+
{
2158+
_disposed = true;
2159+
_taskQueue.Writer.Complete();
2160+
return new ValueTask(_runTask);
2161+
}
2162+
2163+
private async Task Run()
2164+
{
2165+
while (await _taskQueue.Reader.WaitToReadAsync())
2166+
{
2167+
SetSynchronizationContext(this);
2168+
while (_taskQueue.Reader.TryRead(out var tuple))
2169+
{
2170+
var (callback, state) = tuple;
2171+
callback(state);
2172+
}
2173+
SetSynchronizationContext(null);
2174+
}
2175+
}
2176+
}
2177+
20932178
private class PollTrackingMessageHandler : DelegatingHandler
20942179
{
20952180
public Task<HttpResponseMessage> ActivePoll { get; private set; }

0 commit comments

Comments
 (0)