Skip to content

Commit 0d343d9

Browse files
committed
add INCREX integration tests
1 parent c74bef2 commit 0d343d9

3 files changed

Lines changed: 249 additions & 13 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
5+
namespace StackExchange.Redis.Tests;
6+
7+
[RunPerProtocol]
8+
public class IncrexIntegrationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture)
9+
{
10+
[Fact(Timeout = 5000)]
11+
public async Task StringIncrementIncrex_Int64_WithBoundsAndExpiry()
12+
{
13+
await using var conn = Create(require: RedisFeatures.v8_8_0);
14+
var db = conn.GetDatabase();
15+
var key = Me();
16+
db.KeyDelete(key, CommandFlags.FireAndForget);
17+
18+
db.StringSet(key, 10);
19+
20+
var result = await db.StringIncrementAsync(key, 2L, TimeSpan.FromSeconds(5), lowerBound: 0, upperBound: 20);
21+
22+
Assert.Equal(12, result.Value);
23+
Assert.Equal(2, result.AppliedIncrement);
24+
Assert.Equal(12, (long)db.StringGet(key));
25+
Assert.True((await db.KeyTimeToLiveAsync(key)) > TimeSpan.Zero);
26+
}
27+
28+
[Fact(Timeout = 5000)]
29+
public async Task StringIncrementIncrex_Double_WithAbsoluteExpiryAndEnx()
30+
{
31+
await using var conn = Create(require: RedisFeatures.v8_8_0);
32+
var db = conn.GetDatabase();
33+
var key = Me();
34+
var when = DateTime.UtcNow.AddMinutes(30).AddMilliseconds(14);
35+
db.KeyDelete(key, CommandFlags.FireAndForget);
36+
db.StringSet(key, 3.25, TimeSpan.FromMinutes(10));
37+
var beforeTtl = await db.KeyTimeToLiveAsync(key);
38+
39+
var result = await db.StringIncrementAsync(key, 1.25, new Expiration(when, ExpirationFlags.ExpireIfNotExists), lowerBound: -1.5, upperBound: 9.5);
40+
41+
Assert.Equal(4.5, result.Value);
42+
Assert.Equal(1.25, result.AppliedIncrement);
43+
Assert.Equal(4.5, (double)db.StringGet(key));
44+
var afterTtl = await db.KeyTimeToLiveAsync(key);
45+
Assert.NotNull(beforeTtl);
46+
Assert.NotNull(afterTtl);
47+
Assert.True(afterTtl <= beforeTtl);
48+
Assert.True(afterTtl > TimeSpan.FromMinutes(8));
49+
}
50+
51+
[Fact(Timeout = 5000)]
52+
public async Task StringIncrementIncrex_SyncVersion_ParsesResult()
53+
{
54+
await using var conn = Create(require: RedisFeatures.v8_8_0);
55+
var db = conn.GetDatabase();
56+
var key = Me();
57+
db.KeyDelete(key, CommandFlags.FireAndForget);
58+
59+
var result = db.StringIncrement(key, 3L, Expiration.Default);
60+
61+
Assert.Equal(3, result.Value);
62+
Assert.Equal(3, result.AppliedIncrement);
63+
}
64+
65+
[Fact(Timeout = 5000)]
66+
public async Task StringIncrementIncrex_SkipStillAppliesExpiry()
67+
{
68+
await using var conn = Create(require: RedisFeatures.v8_8_0);
69+
var db = conn.GetDatabase();
70+
var key = Me();
71+
db.KeyDelete(key, CommandFlags.FireAndForget);
72+
db.StringSet(key, 5);
73+
74+
var result = await db.StringIncrementAsync(key, 1L, TimeSpan.FromSeconds(5), lowerBound: 10);
75+
76+
Assert.Equal(5, result.Value);
77+
Assert.Equal(0, result.AppliedIncrement);
78+
Assert.True((await db.KeyTimeToLiveAsync(key)) > TimeSpan.Zero);
79+
}
80+
81+
[Fact(Timeout = 5000)]
82+
public async Task StringIncrementIncrex_DefaultClearsExistingTtl()
83+
{
84+
await using var conn = Create(require: RedisFeatures.v8_8_0);
85+
var db = conn.GetDatabase();
86+
var key = Me();
87+
db.KeyDelete(key, CommandFlags.FireAndForget);
88+
db.StringSet(key, 5, TimeSpan.FromMinutes(5));
89+
90+
var result = await db.StringIncrementAsync(key, 2L, Expiration.Default);
91+
92+
Assert.Equal(7, result.Value);
93+
Assert.Equal(2, result.AppliedIncrement);
94+
Assert.Null(await db.KeyTimeToLiveAsync(key));
95+
}
96+
}

tests/StackExchange.Redis.Tests/IncrexTestServer.cs

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
extern alias respite;
2+
using System;
23
using System.Globalization;
34
using respite::RESPite.Messages;
45
using StackExchange.Redis.Server;
56
using Xunit;
67

78
namespace StackExchange.Redis.Tests;
89

9-
public class IncrexTestServer(StringIncrementResult<string> expectedResult, ITestOutputHelper? log = null) : InProcessTestServer(log)
10+
public class IncrexTestServer(ITestOutputHelper? log = null) : InProcessTestServer(log)
1011
{
1112
public sealed class IncrexRequestSnapshot
1213
{
@@ -25,11 +26,17 @@ public sealed class IncrexRequestSnapshot
2526
[RedisCommand(-4, "INCREX")]
2627
protected virtual TypedRedisValue Increx(RedisClient client, in RedisRequest request)
2728
{
28-
var snapshot = new IncrexRequestSnapshot
29-
{
30-
Key = request.GetKey(1),
31-
};
29+
var snapshot = ParseRequest(in request);
30+
LastRequest = snapshot;
3231

32+
return snapshot.IsFloat
33+
? ExecuteDouble(client.Database, snapshot)
34+
: ExecuteInt64(client.Database, snapshot);
35+
}
36+
37+
private IncrexRequestSnapshot ParseRequest(in RedisRequest request)
38+
{
39+
var snapshot = new IncrexRequestSnapshot { Key = request.GetKey(1) };
3340
int index = 2;
3441
while (index < request.Count)
3542
{
@@ -61,12 +68,101 @@ protected virtual TypedRedisValue Increx(RedisClient client, in RedisRequest req
6168
break;
6269
}
6370
}
71+
return snapshot;
72+
}
6473

65-
LastRequest = snapshot;
74+
private TypedRedisValue ExecuteInt64(int database, IncrexRequestSnapshot snapshot)
75+
{
76+
var raw = Get(database, snapshot.Key);
77+
bool existed = !raw.IsNull;
78+
long current = raw.IsNull ? 0 : (long)raw;
79+
long delta = long.Parse(snapshot.Increment, CultureInfo.InvariantCulture);
80+
long? lowerBound = snapshot.LowerBound is null ? null : long.Parse(snapshot.LowerBound, CultureInfo.InvariantCulture);
81+
long? upperBound = snapshot.UpperBound is null ? null : long.Parse(snapshot.UpperBound, CultureInfo.InvariantCulture);
82+
83+
long next = current;
84+
long applied = 0;
85+
86+
try
87+
{
88+
long candidate = checked(current + delta);
89+
if ((!lowerBound.HasValue || candidate >= lowerBound.GetValueOrDefault())
90+
&& (!upperBound.HasValue || candidate <= upperBound.GetValueOrDefault()))
91+
{
92+
next = candidate;
93+
applied = delta;
94+
}
95+
}
96+
catch (OverflowException) { }
97+
98+
ApplyValueAndExpiry(database, snapshot, existed, next);
99+
return MakeResult(next, applied);
100+
}
66101

102+
private TypedRedisValue ExecuteDouble(int database, IncrexRequestSnapshot snapshot)
103+
{
104+
var raw = Get(database, snapshot.Key);
105+
bool existed = !raw.IsNull;
106+
double current = raw.IsNull ? 0D : (double)raw;
107+
double delta = double.Parse(snapshot.Increment, CultureInfo.InvariantCulture);
108+
double? lowerBound = snapshot.LowerBound is null ? null : double.Parse(snapshot.LowerBound, CultureInfo.InvariantCulture);
109+
double? upperBound = snapshot.UpperBound is null ? null : double.Parse(snapshot.UpperBound, CultureInfo.InvariantCulture);
110+
111+
double next = current;
112+
double applied = 0;
113+
114+
double candidate = current + delta;
115+
if ((!lowerBound.HasValue || candidate >= lowerBound.GetValueOrDefault())
116+
&& (!upperBound.HasValue || candidate <= upperBound.GetValueOrDefault()))
117+
{
118+
next = candidate;
119+
applied = delta;
120+
}
121+
122+
ApplyValueAndExpiry(database, snapshot, existed, next);
123+
return MakeResult(next, applied);
124+
}
125+
126+
private void ApplyValueAndExpiry(int database, IncrexRequestSnapshot snapshot, bool existed, RedisValue value)
127+
{
128+
var priorTtl = existed ? Ttl(database, snapshot.Key) : null;
129+
Set(database, snapshot.Key, value);
130+
131+
if (snapshot.ExpiryMode is null)
132+
{
133+
return;
134+
}
135+
136+
if (snapshot.Enx && priorTtl.HasValue && priorTtl.Value != TimeSpan.MaxValue)
137+
{
138+
_ = Expire(database, snapshot.Key, priorTtl.Value);
139+
return;
140+
}
141+
142+
var ttl = snapshot.ExpiryMode switch
143+
{
144+
"EX" => TimeSpan.FromSeconds(long.Parse(snapshot.ExpiryValue!, CultureInfo.InvariantCulture)),
145+
"PX" => TimeSpan.FromMilliseconds(long.Parse(snapshot.ExpiryValue!, CultureInfo.InvariantCulture)),
146+
"EXAT" => DateTimeOffset.FromUnixTimeSeconds(long.Parse(snapshot.ExpiryValue!, CultureInfo.InvariantCulture)).UtcDateTime - Time(),
147+
"PXAT" => DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(snapshot.ExpiryValue!, CultureInfo.InvariantCulture)).UtcDateTime - Time(),
148+
_ => throw new InvalidOperationException("Unknown expiry mode: " + snapshot.ExpiryMode),
149+
};
150+
_ = Expire(database, snapshot.Key, ttl);
151+
}
152+
153+
private static TypedRedisValue MakeResult(long value, long appliedIncrement)
154+
{
155+
var result = TypedRedisValue.Rent(2, out var span, RespPrefix.Array);
156+
span[0] = TypedRedisValue.BulkString((RedisValue)value);
157+
span[1] = TypedRedisValue.BulkString((RedisValue)appliedIncrement);
158+
return result;
159+
}
160+
161+
private static TypedRedisValue MakeResult(double value, double appliedIncrement)
162+
{
67163
var result = TypedRedisValue.Rent(2, out var span, RespPrefix.Array);
68-
span[0] = TypedRedisValue.BulkString(expectedResult.Value);
69-
span[1] = TypedRedisValue.BulkString(expectedResult.AppliedIncrement);
164+
span[0] = TypedRedisValue.BulkString((RedisValue)value);
165+
span[1] = TypedRedisValue.BulkString((RedisValue)appliedIncrement);
70166
return result;
71167
}
72168

tests/StackExchange.Redis.Tests/IncrexUnitTests.cs

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ public class IncrexUnitTests(ITestOutputHelper log)
1212
[Fact]
1313
public async Task StringIncrementIncrex_Int64_WithBoundsAndExpiry()
1414
{
15-
using var server = new IncrexTestServer(new("12", "2"), log);
15+
using var server = new IncrexTestServer(log);
1616
await using var muxer = await server.ConnectAsync();
1717
var db = muxer.GetDatabase();
1818
var key = Me();
1919

20+
db.StringSet(key, 10);
21+
2022
var result = await db.StringIncrementAsync(key, 2L, TimeSpan.FromSeconds(5), lowerBound: 0, upperBound: 20);
2123

2224
Assert.Equal(12, result.Value);
2325
Assert.Equal(2, result.AppliedIncrement);
26+
Assert.Equal(12, (long)db.StringGet(key));
27+
Assert.True((await db.KeyTimeToLiveAsync(key)) > TimeSpan.Zero);
2428

2529
var request = server.LastRequest!;
2630
Assert.Equal(key, request.Key);
@@ -36,16 +40,24 @@ public async Task StringIncrementIncrex_Int64_WithBoundsAndExpiry()
3640
[Fact]
3741
public async Task StringIncrementIncrex_Double_WithAbsoluteExpiryAndEnx()
3842
{
39-
using var server = new IncrexTestServer(new("4.5", "1.25"), log);
43+
using var server = new IncrexTestServer(log);
4044
await using var muxer = await server.ConnectAsync();
4145
var db = muxer.GetDatabase();
4246
var key = Me();
4347
var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc).AddMilliseconds(14);
48+
db.StringSet(key, 3.25, TimeSpan.FromMinutes(10));
49+
var beforeTtl = await db.KeyTimeToLiveAsync(key);
4450

4551
var result = await db.StringIncrementAsync(key, 1.25, new Expiration(when, ExpirationFlags.ExpireIfNotExists), lowerBound: -1.5, upperBound: 9.5);
4652

4753
Assert.Equal(4.5, result.Value);
4854
Assert.Equal(1.25, result.AppliedIncrement);
55+
Assert.Equal(4.5, (double)db.StringGet(key));
56+
var afterTtl = await db.KeyTimeToLiveAsync(key);
57+
Assert.NotNull(beforeTtl);
58+
Assert.NotNull(afterTtl);
59+
Assert.True(afterTtl <= beforeTtl);
60+
Assert.True(afterTtl > TimeSpan.FromMinutes(8));
4961

5062
var request = server.LastRequest!;
5163
Assert.Equal(key, request.Key);
@@ -61,20 +73,52 @@ public async Task StringIncrementIncrex_Double_WithAbsoluteExpiryAndEnx()
6173
[Fact]
6274
public async Task StringIncrementIncrex_SyncVersion_ParsesResult()
6375
{
64-
using var server = new IncrexTestServer(new("10", "0"), log);
76+
using var server = new IncrexTestServer(log);
6577
await using var muxer = await server.ConnectAsync();
6678
var db = muxer.GetDatabase();
6779

6880
var result = db.StringIncrement(Me(), 3L, Expiration.Default);
6981

70-
Assert.Equal(10, result.Value);
82+
Assert.Equal(3, result.Value);
83+
Assert.Equal(3, result.AppliedIncrement);
84+
}
85+
86+
[Fact]
87+
public async Task StringIncrementIncrex_SkipStillAppliesExpiry()
88+
{
89+
using var server = new IncrexTestServer(log);
90+
await using var muxer = await server.ConnectAsync();
91+
var db = muxer.GetDatabase();
92+
var key = Me();
93+
db.StringSet(key, 5);
94+
95+
var result = await db.StringIncrementAsync(key, 1L, TimeSpan.FromSeconds(5), lowerBound: 10);
96+
97+
Assert.Equal(5, result.Value);
7198
Assert.Equal(0, result.AppliedIncrement);
99+
Assert.True((await db.KeyTimeToLiveAsync(key)) > TimeSpan.Zero);
100+
}
101+
102+
[Fact]
103+
public async Task StringIncrementIncrex_DefaultClearsExistingTtl()
104+
{
105+
using var server = new IncrexTestServer(log);
106+
await using var muxer = await server.ConnectAsync();
107+
var db = muxer.GetDatabase();
108+
var key = Me();
109+
db.StringSet(key, 5, TimeSpan.FromMinutes(5));
110+
111+
var result = await db.StringIncrementAsync(key, 2L, Expiration.Default);
112+
113+
Assert.Equal(7, result.Value);
114+
Assert.Equal(2, result.AppliedIncrement);
115+
Assert.Null(await db.KeyTimeToLiveAsync(key));
72116
}
73117

74118
[Fact]
75119
public async Task StringIncrementIncrex_RejectsKeepTtl()
76120
{
77-
using var server = new IncrexTestServer(new("0", "0"), log);
121+
using var server = new IncrexTestServer(log);
78122
await using var muxer = await server.ConnectAsync();
79123
var db = muxer.GetDatabase();
80124

0 commit comments

Comments
 (0)