Files
Gateway-keepaliveForDotnet/tests/GatewayKeepalive.Tests/SwitchPolicyTests.cs
T
chuzhongzai 2d85527656 实现 .NET 10 托盘版网关保活
包含分层探测、可补偿的 IPv4 路由/DNS 切换、自动策略、托盘与分组设置界面、只读诊断工具、离线测试和发布脚本。
2026-10-23 23:35:20 +08:00

156 lines
9.0 KiB
C#

using GatewayKeepalive.Core;
namespace GatewayKeepalive.Tests;
internal static class SwitchPolicyTests
{
public static Task SuccessThresholdAndCooldownAsync()
{
var clock = new ManualTimeProvider();
var policy = new SwitchPolicy(clock);
var settings = Fixtures.Settings() with { SuccessThreshold = 3, RecoveryCooldownSeconds = 10 };
var healthy = Fixtures.Health(true, true);
policy.Switched();
Check.Equal(false, policy.Evaluate(settings, NetworkMode.Direct, healthy) is not null, "The first success must remain below threshold");
Check.Equal(1, policy.SuccessCount, "One success should be counted");
clock.AdvanceSeconds(5);
Check.True(policy.Evaluate(settings, NetworkMode.Direct, healthy) is null, "The second success must remain below threshold");
clock.AdvanceSeconds(4);
Check.True(policy.Evaluate(settings, NetworkMode.Direct, healthy) is null, "The recovery cooldown must still block at 9 seconds");
Check.Equal(3, policy.SuccessCount, "Success count should cap at the configured threshold");
clock.AdvanceSeconds(1);
Check.Equal(true, policy.Evaluate(settings, NetworkMode.Direct, healthy), "The switch must fire exactly when the recovery cooldown expires");
var resetting = new SwitchPolicy(clock);
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "First healthy observation should not switch");
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "Second healthy observation should not switch");
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(false, true)) is null, "An unhealthy observation must reset consecutive success");
Check.Equal(0, resetting.SuccessCount, "Unhealthy observation should reset success count");
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "First post-reset success must not switch");
Check.True(resetting.Evaluate(settings, NetworkMode.Direct, healthy) is null, "Second post-reset success must not switch");
Check.Equal(2, resetting.SuccessCount, "Only consecutive successes should count");
return Task.CompletedTask;
}
public static Task FailureThresholdAndCooldownAsync()
{
var clock = new ManualTimeProvider();
var policy = new SwitchPolicy(clock);
var settings = Fixtures.Settings() with
{
FailureThreshold = 3,
FailureCooldownSeconds = 5,
RecoveryCooldownSeconds = 3600,
};
policy.Switched();
var unhealthy = Fixtures.Health(true, false);
Check.True(policy.Evaluate(settings, NetworkMode.Proxy, unhealthy) is null, "First failed probe must not switch");
clock.AdvanceSeconds(1);
Check.True(policy.Evaluate(settings, NetworkMode.Proxy, unhealthy) is null, "Second failed probe must not switch");
clock.AdvanceSeconds(3);
Check.True(policy.Evaluate(settings, NetworkMode.Proxy, unhealthy) is null, "Failure cooldown must block before 5 seconds");
Check.Equal(3, policy.FailureCount, "Failure count should cap at threshold");
clock.AdvanceSeconds(1);
Check.Equal(false, policy.Evaluate(settings, NetworkMode.Proxy, unhealthy), "Proxy must fail over to direct at its own cooldown");
var recoveryClock = new ManualTimeProvider();
var recovery = new SwitchPolicy(recoveryClock);
var recoverySettings = settings with { SuccessThreshold = 1, RecoveryCooldownSeconds = 40, FailureCooldownSeconds = 3600 };
recovery.Switched();
recoveryClock.AdvanceSeconds(39);
Check.True(recovery.Evaluate(recoverySettings, NetworkMode.Direct, Fixtures.Health(true, true)) is null, "Recovery cooldown must be independent of failure cooldown");
recoveryClock.AdvanceSeconds(1);
Check.Equal(true, recovery.Evaluate(recoverySettings, NetworkMode.Direct, Fixtures.Health(true, true)), "Direct should recover at the configured recovery cooldown");
var resetting = new SwitchPolicy(new ManualTimeProvider());
resetting.Switched();
resetting.Evaluate(settings, NetworkMode.Proxy, unhealthy);
resetting.Evaluate(settings, NetworkMode.Proxy, unhealthy);
Check.Equal(2, resetting.FailureCount, "Failure fixture should be consecutive");
Check.True(resetting.Evaluate(settings, NetworkMode.Proxy, Fixtures.Health(true, true)) is null, "A healthy proxy check must not switch");
Check.Equal(0, resetting.FailureCount, "A healthy proxy check must reset failure count");
resetting.Evaluate(settings, NetworkMode.Proxy, unhealthy);
Check.Equal(1, resetting.FailureCount, "Only consecutive failures should count after a healthy check");
return Task.CompletedTask;
}
public static Task FailureRetryUsesThirtySecondFloorAsync()
{
var clock = new ManualTimeProvider();
var policy = new SwitchPolicy(clock);
var settings = Fixtures.Settings() with { SuccessThreshold = 1, RecoveryCooldownSeconds = 0 };
var healthy = Fixtures.Health(true, true);
policy.Switched();
clock.AdvanceSeconds(1);
policy.Failed();
Check.Equal(0, policy.SuccessCount, "Failed() should reset success count");
Check.Equal(0, policy.FailureCount, "Failed() should reset failure count");
clock.AdvanceSeconds(29);
Check.True(policy.Evaluate(settings, NetworkMode.Direct, healthy) is null, "Recovery must remain blocked 29 seconds after a failure");
Check.Equal(1, policy.SuccessCount, "The blocked attempt should still be observed");
clock.AdvanceSeconds(1);
Check.Equal(true, policy.Evaluate(settings, NetworkMode.Direct, healthy), "Recovery must become available at the 30 second failure backoff");
var failureClock = new ManualTimeProvider();
var failurePolicy = new SwitchPolicy(failureClock);
var failureSettings = settings with { FailureThreshold = 1, FailureCooldownSeconds = 0 };
failurePolicy.Failed();
failureClock.AdvanceSeconds(29);
Check.True(failurePolicy.Evaluate(failureSettings, NetworkMode.Proxy, Fixtures.Health(false, true)) is null, "Failover must also wait 29 seconds");
failureClock.AdvanceSeconds(1);
Check.Equal(false, failurePolicy.Evaluate(failureSettings, NetworkMode.Proxy, Fixtures.Health(false, true)), "Failover must be allowed at the 30 second floor");
return Task.CompletedTask;
}
public static Task NonAutoModesResetCountsAsync()
{
var policy = new SwitchPolicy(new ManualTimeProvider());
var settings = Fixtures.Settings();
var healthy = Fixtures.Health(true, true);
policy.Evaluate(settings, NetworkMode.Direct, healthy);
policy.Evaluate(settings, NetworkMode.Direct, healthy);
Check.Equal(2, policy.SuccessCount, "Fixture should accumulate success before changing mode");
foreach (var mode in new[] { ControlPolicy.Paused, ControlPolicy.ForceDirect, ControlPolicy.ForceProxy })
{
Check.True(policy.Evaluate(settings with { Policy = mode }, NetworkMode.Direct, healthy) is null, $"{mode} must request no automatic switch");
Check.Equal(0, policy.SuccessCount, $"{mode} must clear success count");
Check.Equal(0, policy.FailureCount, $"{mode} must clear failure count");
}
policy.Evaluate(settings, NetworkMode.Proxy, Fixtures.Health(false, true));
Check.True(policy.Evaluate(settings, NetworkMode.Degraded, healthy) is null, "Observe-only network modes must not switch");
Check.Equal(0, policy.FailureCount, "Observe-only modes must clear failure count");
return Task.CompletedTask;
}
public static Task CountsResetAfterEventsAsync()
{
var clock = new ManualTimeProvider();
var policy = new SwitchPolicy(clock);
var settings = Fixtures.Settings();
var sick = Fixtures.Health(true, false);
policy.Evaluate(settings, NetworkMode.Proxy, sick);
policy.Evaluate(settings, NetworkMode.Proxy, sick);
Check.Equal(2, policy.FailureCount, "Failure count fixture");
policy.Switched();
Check.Equal(0, policy.FailureCount, "Switched() should clear failure count");
Check.Equal(0, policy.SuccessCount, "Switched() should clear success count");
policy.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(true, true));
policy.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(true, true));
policy.Failed();
Check.Equal(0, policy.SuccessCount, "Failed() should clear success count");
Check.Equal(0, policy.FailureCount, "Failed() should clear failure count");
policy.Evaluate(settings, NetworkMode.Direct, Fixtures.Health(true, true));
policy.Reset();
Check.Equal(0, policy.SuccessCount, "Reset() should clear success count");
Check.Equal(0, policy.FailureCount, "Reset() should clear failure count");
return Task.CompletedTask;
}
}