Adds a Discord reconnection test

This commit is contained in:
Jordan Brown
2020-03-24 22:53:20 -04:00
parent 26c5632289
commit bc67ad28bf
5 changed files with 86 additions and 23 deletions
@@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the next available <see cref="Message"/> or <see langword="null"/> if the <see cref="IProvider"/> needed to reconnect.</returns>
/// <remarks>Note that private messages will come in the form of <see cref="Channel"/>s not returned in <see cref="MapChannels(IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/></remarks>
/// <remarks>Note that private messages will come in the form of <see cref="Channel"/>s not returned in <see cref="MapChannels(IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/>. Do not <see cref="IDisposable.Dispose"/> the <see cref="IProvider"/> on continuations run from the returned <see cref="Task"/>.</remarks>
Task<Message> NextMessage(CancellationToken cancellationToken);
/// <summary>
@@ -20,6 +20,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
readonly Queue<Message> messageQueue;
/// <summary>
/// Used for synchronizing access to <see cref="reconnectCts"/> and <see cref="reconnectTask"/>.
/// </summary>
readonly object reconnectTaskLock;
/// <summary>
/// <see cref="TaskCompletionSource{TResult}"/> that completes while <see cref="messageQueue"/> isn't empty
/// </summary>
@@ -47,6 +52,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
messageQueue = new Queue<Message>();
nextMessage = new TaskCompletionSource<object>();
reconnectTaskLock = new object();
SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult();
logger.LogTrace("Created.");
}
@@ -106,35 +113,37 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Stops and awaits the <see cref="reconnectTask"/>.
/// </summary>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task StopReconnectionTimer()
Task StopReconnectionTimer()
{
reconnectCts?.Cancel();
reconnectCts?.Dispose();
if (reconnectTask != null)
{
await reconnectTask.ConfigureAwait(false);
reconnectTask = null;
}
lock (reconnectTaskLock)
if (reconnectCts != null)
{
reconnectCts.Cancel();
reconnectCts.Dispose();
reconnectCts = null;
Task reconnectTask = this.reconnectTask;
this.reconnectTask = null;
return reconnectTask;
}
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task SetReconnectInterval(uint reconnectInterval)
public Task SetReconnectInterval(uint reconnectInterval)
{
if (reconnectInterval == 0)
throw new ArgumentOutOfRangeException(nameof(reconnectInterval), reconnectInterval, "Reconnect interval cannot be zero!");
await StopReconnectionTimer().ConfigureAwait(false);
reconnectCts = new CancellationTokenSource();
try
Task stopOldTimerTask;
lock (reconnectTaskLock)
{
stopOldTimerTask = StopReconnectionTimer();
reconnectCts = new CancellationTokenSource();
reconnectTask = ReconnectionLoop(reconnectInterval, reconnectCts.Token);
}
catch
{
reconnectCts.Dispose();
reconnectCts = null;
throw;
}
return stopOldTimerTask;
}
/// <summary>
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Tgstation.Server.Host.Tests")]
[assembly: InternalsVisibleTo("Tgstation.Server.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
@@ -1,8 +1,10 @@
using Discord.WebSocket;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading;
@@ -11,16 +13,67 @@ using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Client;
using Tgstation.Server.Host;
using Tgstation.Server.Host.Components.Chat.Providers;
namespace Tgstation.Server.Tests
{
[TestClass]
[TestCategory("SkipWhenLiveUnitTesting")]
public sealed class IntegrationTest
{
readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
[TestMethod]
public async Task TestAutomaticDiscordReconnection()
{
var discordToken = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN");
if (String.IsNullOrWhiteSpace(discordToken))
Assert.Inconclusive("The TGS4_TEST_DISCORD_TOKEN environment variable must be set to run this test!");
using (var discordProvider = new DiscordProvider(Mock.Of<ILogger<DiscordProvider>>(), discordToken, 1))
{
var connectResult = await discordProvider.Connect(default).ConfigureAwait(false);
Assert.IsTrue(connectResult, "Failed to connect to discord!");
Assert.IsTrue(discordProvider.Connected, "Discord provider is not connected!");
// Forcefully close the connection under the provider's nose
// This will be detected in real life scenarios
DiscordSocketClient socketClient = typeof(DiscordProvider)
.GetField("client", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(discordProvider)
as DiscordSocketClient;
Assert.IsNotNull(socketClient, "Reflection unable to read discord socket client!");
await socketClient.LogoutAsync().ConfigureAwait(false);
Assert.IsFalse(discordProvider.Connected, "Discord provider is still connected!");
try
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(70)))
{
do
{
var message = await discordProvider.NextMessage(cts.Token).ConfigureAwait(false);
if (message == null)
break;
}
while (true);
// Prevents a deadlock coming from having the NextMessage continuation call Dispose
await Task.Yield();
}
}
catch (OperationCanceledException)
{
Assert.Fail("Failed to reconnect within the time period!");
}
Assert.IsTrue(discordProvider.Connected, "Discord provider not connected!");
}
}
[TestMethod]
[TestCategory("SkipWhenLiveUnitTesting")]
public async Task TestUpdate()
{
var updatePathRoot = Path.GetTempFileName();
@@ -95,7 +148,6 @@ namespace Tgstation.Server.Tests
}
[TestMethod]
[TestCategory("SkipWhenLiveUnitTesting")]
public async Task TestStandardOperation()
{
var server = new TestingServer(clientFactory, null);
@@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.0.0" />
</ItemGroup>