mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Fix Auto Relog reconnect lifecycle
This commit is contained in:
parent
c19fdd6634
commit
64c1dc55e0
20 changed files with 1243 additions and 415 deletions
145
MinecraftClient.Tests/AutoRelogRetryPolicyTests.cs
Normal file
145
MinecraftClient.Tests/AutoRelogRetryPolicyTests.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
using MinecraftClient.ChatBots;
|
||||
using MinecraftClient.Scripting;
|
||||
|
||||
namespace MinecraftClient.Tests;
|
||||
|
||||
public sealed class AutoRelogRetryPolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConfigurationUsesUnlimitedRetries()
|
||||
{
|
||||
Assert.Equal(-1, new AutoRelog.Configs().Retries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnlimitedRetriesNeverExhaust()
|
||||
{
|
||||
var policy = new AutoRelogRetryPolicy(new ManualTimeProvider());
|
||||
|
||||
for (int attempt = 1; attempt <= 100; attempt++)
|
||||
{
|
||||
Assert.True(policy.TryReserveAttempt(-1, out int retriesLeft));
|
||||
Assert.Equal(-1, retriesLeft);
|
||||
Assert.Equal(attempt, policy.Attempts);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroRetriesDisablesReconnect()
|
||||
{
|
||||
var policy = new AutoRelogRetryPolicy(new ManualTimeProvider());
|
||||
|
||||
Assert.False(policy.TryReserveAttempt(0, out int retriesLeft));
|
||||
Assert.Equal(0, retriesLeft);
|
||||
Assert.Equal(0, policy.Attempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiniteRetryLimitIsExact()
|
||||
{
|
||||
var policy = new AutoRelogRetryPolicy(new ManualTimeProvider());
|
||||
|
||||
Assert.True(policy.TryReserveAttempt(3, out int firstRetriesLeft));
|
||||
Assert.True(policy.TryReserveAttempt(3, out int secondRetriesLeft));
|
||||
Assert.True(policy.TryReserveAttempt(3, out int thirdRetriesLeft));
|
||||
Assert.False(policy.TryReserveAttempt(3, out int exhaustedRetriesLeft));
|
||||
|
||||
Assert.Equal(2, firstRetriesLeft);
|
||||
Assert.Equal(1, secondRetriesLeft);
|
||||
Assert.Equal(0, thirdRetriesLeft);
|
||||
Assert.Equal(0, exhaustedRetriesLeft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectedRestartDoesNotConsumeRetry()
|
||||
{
|
||||
var policy = new AutoRelogRetryPolicy(new ManualTimeProvider());
|
||||
|
||||
Assert.True(policy.TryReserveAttempt(1, out _));
|
||||
policy.RollBackReservedAttempt();
|
||||
|
||||
Assert.True(policy.TryReserveAttempt(1, out int retriesLeft));
|
||||
Assert.Equal(0, retriesLeft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StableConnectionResetsRetryBudget()
|
||||
{
|
||||
var timeProvider = new ManualTimeProvider();
|
||||
var policy = new AutoRelogRetryPolicy(timeProvider);
|
||||
Assert.True(policy.TryReserveAttempt(1, out _));
|
||||
|
||||
policy.MarkJoined();
|
||||
timeProvider.Advance(AutoRelogRetryPolicy.StableConnectionThreshold - TimeSpan.FromMilliseconds(1));
|
||||
Assert.False(policy.ResetAfterStableConnection());
|
||||
|
||||
timeProvider.Advance(TimeSpan.FromMilliseconds(1));
|
||||
Assert.True(policy.ResetAfterStableConnection());
|
||||
Assert.Equal(0, policy.Attempts);
|
||||
Assert.True(policy.TryReserveAttempt(1, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransportLossAlwaysReconnectsWhenEnabled()
|
||||
{
|
||||
bool reconnect = AutoRelogRetryPolicy.ShouldReconnect(
|
||||
ChatBot.DisconnectReason.ConnectionLost,
|
||||
"A transport-specific error without a configured phrase",
|
||||
ignoreKickMessage: false,
|
||||
["Server is restarting"],
|
||||
out string? matchedMessage);
|
||||
|
||||
Assert.True(reconnect);
|
||||
Assert.Null(matchedMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ChatBot.DisconnectReason.InGameKick)]
|
||||
[InlineData(ChatBot.DisconnectReason.LoginRejected)]
|
||||
public void ServerMessageMatchingIsCaseInsensitive(ChatBot.DisconnectReason reason)
|
||||
{
|
||||
bool reconnect = AutoRelogRetryPolicy.ShouldReconnect(
|
||||
reason,
|
||||
"THE SERVER IS RESTARTING NOW",
|
||||
ignoreKickMessage: false,
|
||||
["server is restarting"],
|
||||
out string? matchedMessage);
|
||||
|
||||
Assert.True(reconnect);
|
||||
Assert.Equal("server is restarting", matchedMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserLogoutNeverReconnects()
|
||||
{
|
||||
Assert.False(AutoRelogRetryPolicy.ShouldReconnect(
|
||||
ChatBot.DisconnectReason.UserLogout,
|
||||
"Server is restarting",
|
||||
ignoreKickMessage: true,
|
||||
["Server is restarting"],
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoreKickMessageAllowsNonmatchingServerKick()
|
||||
{
|
||||
Assert.True(AutoRelogRetryPolicy.ShouldReconnect(
|
||||
ChatBot.DisconnectReason.InGameKick,
|
||||
"Administrative removal",
|
||||
ignoreKickMessage: true,
|
||||
[],
|
||||
out _));
|
||||
}
|
||||
|
||||
private sealed class ManualTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset utcNow = DateTimeOffset.UnixEpoch;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => utcNow;
|
||||
|
||||
internal void Advance(TimeSpan duration)
|
||||
{
|
||||
utcNow += duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
MinecraftClient.Tests/MinecraftClient.Tests.csproj
Normal file
22
MinecraftClient.Tests/MinecraftClient.Tests.csproj
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MinecraftClient\MinecraftClient.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
61
MinecraftClient.Tests/RestartCoordinatorTests.cs
Normal file
61
MinecraftClient.Tests/RestartCoordinatorTests.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
namespace MinecraftClient.Tests;
|
||||
|
||||
public sealed class RestartCoordinatorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CoalescesSameAttemptAndQueuesNewerAttempt()
|
||||
{
|
||||
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var secondCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
using var coordinator = new RestartCoordinator(
|
||||
async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.ConnectionAttempt == 10)
|
||||
{
|
||||
firstStarted.SetResult();
|
||||
await releaseFirst.Task.WaitAsync(cancellationToken);
|
||||
}
|
||||
else if (request.ConnectionAttempt == 11)
|
||||
{
|
||||
secondCompleted.SetResult();
|
||||
}
|
||||
},
|
||||
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
||||
|
||||
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
|
||||
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.False(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
|
||||
Assert.True(coordinator.TrySchedule(new RestartRequest(11, TimeSpan.Zero, true)));
|
||||
Assert.True(coordinator.HasScheduledRestart(11));
|
||||
|
||||
releaseFirst.SetResult();
|
||||
await secondCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsStaleAttempt()
|
||||
{
|
||||
using var coordinator = new RestartCoordinator(
|
||||
(_, _) => Task.CompletedTask,
|
||||
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
||||
|
||||
Assert.True(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
|
||||
Assert.False(coordinator.TrySchedule(new RestartRequest(19, TimeSpan.Zero, true)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TerminalStopRejectsFurtherRestarts()
|
||||
{
|
||||
using var coordinator = new RestartCoordinator(
|
||||
(_, _) => Task.CompletedTask,
|
||||
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
||||
|
||||
coordinator.Stop();
|
||||
|
||||
Assert.False(coordinator.TrySchedule(new RestartRequest(1, TimeSpan.Zero, true)));
|
||||
Assert.False(coordinator.HasScheduledRestart(1));
|
||||
}
|
||||
}
|
||||
34
MinecraftClient.Tests/SocketWrapperTests.cs
Normal file
34
MinecraftClient.Tests/SocketWrapperTests.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
|
||||
namespace MinecraftClient.Tests;
|
||||
|
||||
public sealed class SocketWrapperTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GracefulPeerCloseEndsReadInsteadOfSpinning()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
Task<TcpClient> acceptTask = listener.AcceptTcpClientAsync();
|
||||
await client.ConnectAsync((IPEndPoint)listener.LocalEndpoint);
|
||||
using TcpClient peer = await acceptTask;
|
||||
var wrapper = new SocketWrapper(client);
|
||||
|
||||
peer.Client.Shutdown(SocketShutdown.Both);
|
||||
peer.Close();
|
||||
|
||||
Assert.True(SpinWait.SpinUntil(wrapper.HasDataAvailable, TimeSpan.FromSeconds(5)));
|
||||
await Assert.ThrowsAsync<EndOfStreamException>(
|
||||
() => Task.Run(() => wrapper.ReadDataRAW(1)).WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
1
MinecraftClient.Tests/Usings.cs
Normal file
1
MinecraftClient.Tests/Usings.cs
Normal file
|
|
@ -0,0 +1 @@
|
|||
global using Xunit;
|
||||
Loading…
Add table
Add a link
Reference in a new issue