diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index 016480c59..118e0f975 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -146,6 +146,7 @@ public async Task TestIdentity() [Fact] public async Task IntentionalWrongServer() { + SkipOnWindowsRelease(); static string? StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) => (string?)server.Execute(0, "GET", [key], flags); diff --git a/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs b/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs index e967de388..258110a6e 100644 --- a/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs @@ -8,22 +8,35 @@ namespace StackExchange.Redis.Tests; [RunPerProtocol] public class KeyIdleAsyncTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { + // Target the standalone secure server (6381) rather than the default primary (6379). + // OBJECT IDLETIME is reset via the value's LRU access clock, but Redis deliberately + // suppresses that update while a save/AOF/replication-sync child is active (copy-on-write + // avoidance). The default primary has a replica attached, and every replica full-sync forks + // such a child; if one overlaps a touch/read in these tests, the idle time isn't reset and the + // test flakes (observed on Windows CI). 6381 has no replica and no persistence, so no fork ever + // suppresses the reset and the behaviour is deterministic. Overriding GetConfiguration here also + // opts these tests out of the shared connection fixture, giving each a dedicated 6381 connection. + protected override string GetConfiguration() + => TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; + [Fact] public async Task IdleTimeAsync() { + SkipOnWindowsRelease("WSL exacerbates replication time"); await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + var timer = Stopwatch.StartNew(); await Task.Delay(2000).ForAwait(); var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime > TimeSpan.Zero, "First check"); + Assert.True(idleTime > TimeSpan.Zero, $"First check: {idleTime} should be > 0; elapsed: {timer.ElapsedMilliseconds}ms"); db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); var idleTime2 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime2 < idleTime, "Second check"); + Assert.True(idleTime2 < idleTime, $"Second check: {idleTime2} should be < {idleTime}; elapsed: {timer.ElapsedMilliseconds}ms"); db.KeyDelete(key); var idleTime3 = await db.KeyIdleTimeAsync(key).ForAwait(); @@ -33,6 +46,7 @@ public async Task IdleTimeAsync() [Fact] public async Task TouchIdleTimeAsync() { + SkipOnWindowsRelease("WSL exacerbates replication time"); await using var conn = Create(require: RedisFeatures.v3_2_1); RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/KeyIdleTests.cs b/tests/StackExchange.Redis.Tests/KeyIdleTests.cs index deec1efb4..9c408b4e8 100644 --- a/tests/StackExchange.Redis.Tests/KeyIdleTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyIdleTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Xunit; @@ -7,22 +8,35 @@ namespace StackExchange.Redis.Tests; [RunPerProtocol] public class KeyIdleTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { + // Target the standalone secure server (6381) rather than the default primary (6379). + // OBJECT IDLETIME is reset via the value's LRU access clock, but Redis deliberately + // suppresses that update while a save/AOF/replication-sync child is active (copy-on-write + // avoidance). The default primary has a replica attached, and every replica full-sync forks + // such a child; if one overlaps a touch/read in these tests, the idle time isn't reset and the + // test flakes (observed on Windows CI). 6381 has no replica and no persistence, so no fork ever + // suppresses the reset and the behaviour is deterministic. Overriding GetConfiguration here also + // opts these tests out of the shared connection fixture, giving each a dedicated 6381 connection. + protected override string GetConfiguration() + => TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; + [Fact] public async Task IdleTime() { + SkipOnWindowsRelease("WSL exacerbates replication time"); await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + var timer = Stopwatch.StartNew(); await Task.Delay(2000).ForAwait(); var idleTime = db.KeyIdleTime(key); - Assert.True(idleTime > TimeSpan.Zero); + Assert.True(idleTime > TimeSpan.Zero, $"First check: {idleTime} should be > 0; elapsed: {timer.ElapsedMilliseconds}ms"); db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); var idleTime2 = db.KeyIdleTime(key); - Assert.True(idleTime2 < idleTime); + Assert.True(idleTime2 < idleTime, $"Second check: {idleTime2} should be < {idleTime}; elapsed: {timer.ElapsedMilliseconds}ms"); db.KeyDelete(key); var idleTime3 = db.KeyIdleTime(key); @@ -32,18 +46,20 @@ public async Task IdleTime() [Fact] public async Task TouchIdleTime() { + SkipOnWindowsRelease("WSL exacerbates replication time"); await using var conn = Create(require: RedisFeatures.v3_2_1); RedisKey key = Me(); var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + var timer = Stopwatch.StartNew(); await Task.Delay(2000).ForAwait(); var idleTime = db.KeyIdleTime(key); - Assert.True(idleTime > TimeSpan.Zero, "First check"); + Assert.True(idleTime > TimeSpan.Zero, $"First check: {idleTime} should be > 0; elapsed: {timer.ElapsedMilliseconds}ms"); - Assert.True(db.KeyTouch(key), "Second check"); + Assert.True(db.KeyTouch(key), $"Second check: should be True; elapsed: {timer.ElapsedMilliseconds}ms"); var idleTime1 = db.KeyIdleTime(key); - Assert.True(idleTime1 < idleTime, "Third check"); + Assert.True(idleTime1 < idleTime, $"Third check: {idleTime1} should be < {idleTime}; elapsed: {timer.ElapsedMilliseconds}ms"); } } diff --git a/tests/StackExchange.Redis.Tests/RoleTests.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs index 198ae6da7..92f66e0e4 100644 --- a/tests/StackExchange.Redis.Tests/RoleTests.cs +++ b/tests/StackExchange.Redis.Tests/RoleTests.cs @@ -13,6 +13,7 @@ public class Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : [InlineData(false)] public async Task PrimaryRole(bool allowAdmin) // should work with or without admin now { + SkipOnWindowsRelease(); await using var conn = Create(allowAdmin: allowAdmin); var servers = conn.GetServers(); Log("Server list:"); diff --git a/tests/StackExchange.Redis.Tests/SentinelTests.cs b/tests/StackExchange.Redis.Tests/SentinelTests.cs index 039df4335..a53194169 100644 --- a/tests/StackExchange.Redis.Tests/SentinelTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelTests.cs @@ -12,6 +12,7 @@ public class SentinelTests(ITestOutputHelper output) : SentinelBase(output) [Fact] public async Task PrimaryConnectTest() { + SkipOnWindowsRelease(); var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = ConnectionMultiplexer.Connect(connectionString); @@ -49,6 +50,7 @@ public async Task PrimaryConnectTest() [Fact] public async Task PrimaryConnectAsyncTest() { + SkipOnWindowsRelease(); var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); @@ -86,6 +88,7 @@ public async Task PrimaryConnectAsyncTest() [RunPerProtocol] public async Task SentinelConnectTest() { + SkipOnWindowsRelease(); var options = ServiceOptions.Clone(); options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); await using var conn = ConnectionMultiplexer.SentinelConnect(options); @@ -98,6 +101,7 @@ public async Task SentinelConnectTest() [Fact] public async Task SentinelRepeatConnectTest() { + SkipOnWindowsRelease(); var options = ConfigurationOptions.Parse($"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA}"); options.ServiceName = ServiceName; options.AllowAdmin = true; @@ -130,6 +134,7 @@ public async Task SentinelRepeatConnectTest() [Fact] public async Task SentinelConnectAsyncTest() { + SkipOnWindowsRelease(); var options = ServiceOptions.Clone(); options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); var conn = await ConnectionMultiplexer.SentinelConnectAsync(options); @@ -142,6 +147,7 @@ public async Task SentinelConnectAsyncTest() [Fact] public void SentinelRole() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var role = server.Role(); @@ -155,6 +161,7 @@ public void SentinelRole() [Fact] public async Task PingTest() { + SkipOnWindowsRelease(); var test = await SentinelServerA.PingAsync(); Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); test = await SentinelServerB.PingAsync(); @@ -166,6 +173,7 @@ public async Task PingTest() [Fact] public void SentinelGetPrimaryAddressByNameTest() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var primary = server.SentinelMaster(ServiceName); @@ -182,6 +190,7 @@ public void SentinelGetPrimaryAddressByNameTest() [Fact] public async Task SentinelGetPrimaryAddressByNameAsyncTest() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var primary = server.SentinelMaster(ServiceName); @@ -198,6 +207,7 @@ public async Task SentinelGetPrimaryAddressByNameAsyncTest() [Fact] public void SentinelGetMasterAddressByNameNegativeTest() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var endpoint = server.SentinelGetMasterAddressByName("FakeServiceName"); @@ -208,6 +218,7 @@ public void SentinelGetMasterAddressByNameNegativeTest() [Fact] public async Task SentinelGetMasterAddressByNameAsyncNegativeTest() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var endpoint = await server.SentinelGetMasterAddressByNameAsync("FakeServiceName").ForAwait(); @@ -218,6 +229,7 @@ public async Task SentinelGetMasterAddressByNameAsyncNegativeTest() [Fact] public void SentinelPrimaryTest() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var dict = server.SentinelMaster(ServiceName).ToDictionary(); @@ -233,6 +245,7 @@ public void SentinelPrimaryTest() [Fact] public async Task SentinelPrimaryAsyncTest() { + SkipOnWindowsRelease(); foreach (var server in SentinelsServers) { var results = await server.SentinelMasterAsync(ServiceName).ForAwait(); @@ -248,6 +261,7 @@ public async Task SentinelPrimaryAsyncTest() [Fact] public void SentinelSentinelsTest() { + SkipOnWindowsRelease(); var sentinels = SentinelServerA.SentinelSentinels(ServiceName); var expected = new List @@ -305,6 +319,7 @@ public void SentinelSentinelsTest() [Fact] public async Task SentinelSentinelsAsyncTest() { + SkipOnWindowsRelease(); var sentinels = await SentinelServerA.SentinelSentinelsAsync(ServiceName).ForAwait(); var expected = new List { @@ -363,6 +378,7 @@ public async Task SentinelSentinelsAsyncTest() [Fact] public void SentinelPrimariesTest() { + SkipOnWindowsRelease(); var primaryConfigs = SentinelServerA.SentinelMasters(); Assert.Single(primaryConfigs); Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); @@ -380,6 +396,7 @@ public void SentinelPrimariesTest() [Fact] public async Task SentinelPrimariesAsyncTest() { + SkipOnWindowsRelease(); var primaryConfigs = await SentinelServerA.SentinelMastersAsync().ForAwait(); Assert.Single(primaryConfigs); Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); @@ -397,6 +414,7 @@ public async Task SentinelPrimariesAsyncTest() [Fact] public async Task SentinelReplicasTest() { + SkipOnWindowsRelease(); // Give previous test run a moment to reset when multi-framework failover is in play. await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); @@ -417,6 +435,7 @@ public async Task SentinelReplicasTest() [Fact] public async Task SentinelReplicasAsyncTest() { + SkipOnWindowsRelease(); // Give previous test run a moment to reset when multi-framework failover is in play. await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); @@ -436,6 +455,7 @@ public async Task SentinelReplicasAsyncTest() [Fact] public async Task SentinelGetSentinelAddressesTest() { + SkipOnWindowsRelease(); var addresses = await SentinelServerA.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); Assert.Contains(SentinelServerB.EndPoint, addresses); Assert.Contains(SentinelServerC.EndPoint, addresses); @@ -452,6 +472,7 @@ public async Task SentinelGetSentinelAddressesTest() [Fact] public async Task ReadOnlyConnectionReplicasTest() { + SkipOnWindowsRelease(); var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); if (replicas.Length == 0) { diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 4106fba98..5a5ffe691 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis.Configuration; @@ -702,4 +703,10 @@ public ValueTask DisposeAsync() return default; } } + + [Conditional("RELEASE")] + protected void SkipOnWindowsRelease(string? message = null) // typically used for tests that are super brittle on the Windows CI + { + Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), message ?? "skipping on Windows"); + } }