276 lines
11 KiB
C#
276 lines
11 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Threading.Channels;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Sockets;
|
|
using Microsoft.AspNetCore.Sockets.Client;
|
|
using Microsoft.AspNetCore.Sockets.Client.Http;
|
|
using Microsoft.AspNetCore.Sockets.Client.Tests;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|
{
|
|
public partial class HttpConnectionTests
|
|
{
|
|
// Nested class for grouping
|
|
public class AbortAsync
|
|
{
|
|
[Fact]
|
|
public async Task AbortAsyncTriggersClosedEventWithException()
|
|
{
|
|
var connection = CreateConnection(out var closedTask);
|
|
try
|
|
{
|
|
// Start the connection
|
|
await connection.StartAsync().OrTimeout();
|
|
|
|
// Abort with an error
|
|
var expected = new Exception("Ruh roh!");
|
|
await connection.AbortAsync(expected).OrTimeout();
|
|
|
|
// Verify that it is thrown
|
|
var actual = await Assert.ThrowsAsync<Exception>(async () => await closedTask.OrTimeout());
|
|
Assert.Same(expected, actual);
|
|
}
|
|
finally
|
|
{
|
|
// Dispose should be clean and exception free.
|
|
await connection.DisposeAsync().OrTimeout();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AbortAsyncWhileStoppingTriggersClosedEventWithException()
|
|
{
|
|
var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(2, out var syncPoints));
|
|
|
|
try
|
|
{
|
|
// Start the connection
|
|
await connection.StartAsync().OrTimeout();
|
|
|
|
// Stop normally
|
|
var stopTask = connection.StopAsync().OrTimeout();
|
|
|
|
// Wait to reach the first sync point
|
|
await syncPoints[0].WaitForSyncPoint().OrTimeout();
|
|
|
|
// Abort with an error
|
|
var expected = new Exception("Ruh roh!");
|
|
var abortTask = connection.AbortAsync(expected).OrTimeout();
|
|
|
|
// Wait for the sync point to hit again
|
|
await syncPoints[1].WaitForSyncPoint().OrTimeout();
|
|
|
|
// Release sync point 0
|
|
syncPoints[0].Continue();
|
|
|
|
// We should close with the error from Abort (because it was set by the call to Abort even though Stop triggered the close)
|
|
var actual = await Assert.ThrowsAsync<Exception>(async () => await closedTask.OrTimeout());
|
|
Assert.Same(expected, actual);
|
|
|
|
// Clean-up
|
|
syncPoints[1].Continue();
|
|
await Task.WhenAll(stopTask, abortTask).OrTimeout();
|
|
}
|
|
finally
|
|
{
|
|
// Dispose should be clean and exception free.
|
|
await connection.DisposeAsync().OrTimeout();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StopAsyncWhileAbortingTriggersClosedEventWithoutException()
|
|
{
|
|
var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(2, out var syncPoints));
|
|
|
|
try
|
|
{
|
|
// Start the connection
|
|
await connection.StartAsync().OrTimeout();
|
|
|
|
// Abort with an error
|
|
var expected = new Exception("Ruh roh!");
|
|
var abortTask = connection.AbortAsync(expected).OrTimeout();
|
|
|
|
// Wait to reach the first sync point
|
|
await syncPoints[0].WaitForSyncPoint().OrTimeout();
|
|
|
|
// Stop normally, without a sync point.
|
|
// This should clear the exception, meaning Closed will not "throw"
|
|
syncPoints[1].Continue();
|
|
await connection.StopAsync();
|
|
await closedTask.OrTimeout();
|
|
|
|
// Clean-up
|
|
syncPoints[0].Continue();
|
|
await abortTask.OrTimeout();
|
|
}
|
|
finally
|
|
{
|
|
// Dispose should be clean and exception free.
|
|
await connection.DisposeAsync().OrTimeout();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StartAsyncCannotBeCalledWhileAbortAsyncInProgress()
|
|
{
|
|
var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(out var syncPoint));
|
|
|
|
try
|
|
{
|
|
// Start the connection
|
|
await connection.StartAsync().OrTimeout();
|
|
|
|
// Abort with an error
|
|
var expected = new Exception("Ruh roh!");
|
|
var abortTask = connection.AbortAsync(expected).OrTimeout();
|
|
|
|
// Wait to reach the first sync point
|
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync().OrTimeout());
|
|
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", ex.Message);
|
|
|
|
// Release the sync point and wait for close to complete
|
|
// (it will throw the abort exception)
|
|
syncPoint.Continue();
|
|
await abortTask.OrTimeout();
|
|
var actual = await Assert.ThrowsAsync<Exception>(() => closedTask.OrTimeout());
|
|
Assert.Same(expected, actual);
|
|
|
|
// We can start now
|
|
await connection.StartAsync().OrTimeout();
|
|
|
|
// And we can stop without getting the abort exception.
|
|
await connection.StopAsync().OrTimeout();
|
|
}
|
|
finally
|
|
{
|
|
// Dispose should be clean and exception free.
|
|
await connection.DisposeAsync().OrTimeout();
|
|
}
|
|
}
|
|
|
|
private HttpConnection CreateConnection(out Task closedTask, Func<Task> stopHandler = null)
|
|
{
|
|
var httpHandler = new TestHttpMessageHandler();
|
|
var transportFactory = new TestTransportFactory(new TestTransport(stopHandler));
|
|
var connection = new HttpConnection(
|
|
new Uri("http://fakeuri.org/"),
|
|
transportFactory,
|
|
NullLoggerFactory.Instance,
|
|
new HttpOptions()
|
|
{
|
|
HttpMessageHandler = httpHandler,
|
|
});
|
|
|
|
var closedTcs = new TaskCompletionSource<object>();
|
|
connection.Closed += ex =>
|
|
{
|
|
if (ex != null)
|
|
{
|
|
closedTcs.SetException(ex);
|
|
}
|
|
else
|
|
{
|
|
closedTcs.SetResult(null);
|
|
}
|
|
};
|
|
closedTask = closedTcs.Task;
|
|
|
|
return connection;
|
|
}
|
|
|
|
private class TestTransport : ITransport
|
|
{
|
|
private Channel<byte[], SendMessage> _application;
|
|
private readonly Func<Task> _stopHandler;
|
|
|
|
public TransferMode? Mode => TransferMode.Text;
|
|
|
|
public TestTransport(Func<Task> stopHandler)
|
|
{
|
|
_stopHandler = stopHandler ?? new Func<Task>(() => Task.CompletedTask);
|
|
}
|
|
|
|
public Task StartAsync(Uri url, Channel<byte[], SendMessage> application, TransferMode requestedTransferMode, string connectionId, IConnection connection)
|
|
{
|
|
_application = application;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task StopAsync()
|
|
{
|
|
await _stopHandler();
|
|
_application.Writer.TryComplete();
|
|
}
|
|
}
|
|
|
|
// Possibly useful as a general-purpose async testing helper?
|
|
private class SyncPoint
|
|
{
|
|
private TaskCompletionSource<object> _atSyncPoint = new TaskCompletionSource<object>();
|
|
private TaskCompletionSource<object> _continueFromSyncPoint = new TaskCompletionSource<object>();
|
|
|
|
// Used by the test code to wait and continue
|
|
public Task WaitForSyncPoint() => _atSyncPoint.Task;
|
|
public void Continue() => _continueFromSyncPoint.TrySetResult(null);
|
|
|
|
// Used by the code under test to wait for the test code to release it.
|
|
public Task WaitToContinue()
|
|
{
|
|
_atSyncPoint.TrySetResult(null);
|
|
return _continueFromSyncPoint.Task;
|
|
}
|
|
|
|
public static Func<Task> Create(out SyncPoint syncPoint)
|
|
{
|
|
var handler = Create(1, out var syncPoints);
|
|
syncPoint = syncPoints[0];
|
|
return handler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a re-entrant function that waits for sync points in sequence.
|
|
/// </summary>
|
|
/// <param name="count">The number of sync points to expect</param>
|
|
/// <param name="syncPoints">The <see cref="SyncPoint"/> objects that can be used to coordinate the sync point</param>
|
|
/// <returns></returns>
|
|
public static Func<Task> Create(int count, out SyncPoint[] syncPoints)
|
|
{
|
|
// Need to use a local so the closure can capture it. You can't use out vars in a closure.
|
|
var localSyncPoints = new SyncPoint[count];
|
|
for (var i = 0; i < count; i += 1)
|
|
{
|
|
localSyncPoints[i] = new SyncPoint();
|
|
}
|
|
|
|
syncPoints = localSyncPoints;
|
|
|
|
var counter = 0;
|
|
return () =>
|
|
{
|
|
if (counter >= localSyncPoints.Length)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
else
|
|
{
|
|
var syncPoint = localSyncPoints[counter];
|
|
|
|
counter += 1;
|
|
return syncPoint.WaitToContinue();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|