Skip to content

Commit bcc0b55

Browse files
committed
tidy TFMs
1 parent cba9bae commit bcc0b55

4 files changed

Lines changed: 285 additions & 6 deletions

File tree

.github/workflows/codeql.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ jobs:
3939
uses: actions/setup-dotnet@v4
4040
with:
4141
dotnet-version: |
42-
8.0.x
4342
10.0.x
4443
4544
# Initializes the CodeQL tools for scanning.

Build.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Project Sdk="Microsoft.Build.Traversal/3.0.2">
22
<ItemGroup>
3+
<ProjectReference Include="eng\**\*.csproj" />
34
<ProjectReference Include="src\**\*.csproj" />
45
<ProjectReference Include="tests\**\*.csproj" />
56
<ProjectReference Include="toys\**\*.csproj" />

src/StackExchange.Redis/StackExchange.Redis.csproj

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<Nullable>enable</Nullable>
44
<!-- extend the default lib targets for the main lib; mostly because of "vectors" -->
5-
<TargetFrameworks>net461;netstandard2.0;net472;netcoreapp3.1;net6.0;net8.0</TargetFrameworks>
5+
<TargetFrameworks>net461;netstandard2.0;net472;net6.0;net8.0;net10.0</TargetFrameworks>
66
<Description>High performance Redis client, incorporating both synchronous and asynchronous usage.</Description>
77
<AssemblyName>StackExchange.Redis</AssemblyName>
88
<AssemblyTitle>StackExchange.Redis</AssemblyTitle>
@@ -41,10 +41,11 @@
4141

4242
<ItemGroup>
4343
<!-- APIs for all target frameworks -->
44-
<AdditionalFiles Include="PublicAPI/PublicAPI.Shipped.txt" />
45-
<AdditionalFiles Include="PublicAPI/PublicAPI.Unshipped.txt" />
46-
<!-- APIs for netcoreapp3.1+ -->
47-
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Shipped.txt" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
44+
<AdditionalFiles Include="PublicAPI/PublicAPI.*.txt" />
45+
<!-- progressively additive APIs in later frameworks -->
46+
<AdditionalFiles Include="PublicAPI/net6.0/PublicAPI.*.txt" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))" />
47+
<!-- for example... -->
48+
<!-- <AdditionalFiles Include="PublicAPI/net10.0/PublicAPI.*.txt" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))" /> -->
4849
</ItemGroup>
4950

5051
<ItemGroup>
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Collections.Generic;
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Linq;
6+
using System.Text;
7+
using RESPite;
8+
using RESPite.Messages;
9+
10+
namespace StackExchange.Redis;
11+
12+
/// <summary>
13+
/// Allows unit testing RESP formatting and parsing.
14+
/// </summary>
15+
[Experimental(Experiments.UnitTesting, UrlFormat = Experiments.UrlFormat)]
16+
public class TestHarness(CommandMap? commandMap = null, RedisChannel channelPrefix = default, RedisKey keyPrefix = default)
17+
{
18+
/// <summary>
19+
/// Channel prefix to use when writing <see cref="RedisChannel"/> values.
20+
/// </summary>
21+
public RedisChannel ChannelPrefix { get; } = channelPrefix;
22+
23+
/// <summary>
24+
/// Channel prefix to use when writing <see cref="RedisChannel"/> values.
25+
/// </summary>
26+
public RedisKey KeyPrefix => _keyPrefix;
27+
private readonly byte[]? _keyPrefix = keyPrefix;
28+
29+
/// <summary>
30+
/// The command map to use when writing root commands.
31+
/// </summary>
32+
public CommandMap CommandMap { get; } = commandMap ?? CommandMap.Default;
33+
34+
/// <summary>
35+
/// Write a RESP frame from a command and set of arguments.
36+
/// </summary>
37+
public byte[] Write(string command, params ICollection<object> args)
38+
{
39+
var msg = new RedisDatabase.ExecuteMessage(CommandMap, -1, CommandFlags.None, command, Fixup(args));
40+
var writer = new MessageWriter(ChannelPrefix, CommandMap, MessageWriter.BlockBuffer);
41+
ReadOnlyMemory<byte> payload = default;
42+
try
43+
{
44+
msg.WriteTo(writer);
45+
payload = MessageWriter.FlushBlockBuffer();
46+
return payload.Span.ToArray();
47+
}
48+
catch
49+
{
50+
MessageWriter.RevertBlockBuffer();
51+
throw;
52+
}
53+
finally
54+
{
55+
MessageWriter.ReleaseBlockBuffer(payload);
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Write a RESP frame from a command and set of arguments.
61+
/// </summary>
62+
public void Write(IBufferWriter<byte> target, string command, params ICollection<object> args)
63+
{
64+
// if we're using someone else's buffer writer, then we don't need to worry about our local
65+
// memory-management rules
66+
if (target is null) throw new ArgumentNullException(nameof(target));
67+
var msg = new RedisDatabase.ExecuteMessage(CommandMap, -1, CommandFlags.None, command, Fixup(args));
68+
var writer = new MessageWriter(ChannelPrefix, CommandMap, target);
69+
msg.WriteTo(writer);
70+
}
71+
72+
/// <summary>
73+
/// Report a validation failure.
74+
/// </summary>
75+
protected virtual void OnValidateFail(in RedisKey expected, in RedisKey actual)
76+
=> throw new InvalidOperationException($"Routing key is not equal: '{expected}' vs '{actual}' (hint: override {nameof(OnValidateFail)})");
77+
78+
/// <summary>
79+
/// Report a validation failure.
80+
/// </summary>
81+
protected virtual void OnValidateFail(string expected, string actual)
82+
=> throw new InvalidOperationException($"RESP is not equal: '{expected}' vs '{actual}' (hint: override {nameof(OnValidateFail)})");
83+
84+
/// <summary>
85+
/// Report a validation failure.
86+
/// </summary>
87+
protected virtual void OnValidateFail(ReadOnlyMemory<byte> expected, ReadOnlyMemory<byte> actual)
88+
=> OnValidateFail(Encoding.UTF8.GetString(expected.Span), Encoding.UTF8.GetString(actual.Span));
89+
90+
/// <summary>
91+
/// Write a RESP frame from a command and set of arguments, and allow a callback to validate
92+
/// the RESP content.
93+
/// </summary>
94+
public void ValidateResp(ReadOnlySpan<byte> expected, string command, params ICollection<object> args)
95+
{
96+
var msg = new RedisDatabase.ExecuteMessage(CommandMap, -1, CommandFlags.None, command, Fixup(args));
97+
var writer = new MessageWriter(ChannelPrefix, CommandMap, MessageWriter.BlockBuffer);
98+
ReadOnlyMemory<byte> actual = default;
99+
byte[]? lease = null;
100+
try
101+
{
102+
msg.WriteTo(writer);
103+
actual = MessageWriter.FlushBlockBuffer();
104+
if (!expected.SequenceEqual(actual.Span))
105+
{
106+
lease = ArrayPool<byte>.Shared.Rent(expected.Length);
107+
expected.CopyTo(lease);
108+
OnValidateFail(lease.AsMemory(0, expected.Length), lease);
109+
}
110+
}
111+
catch
112+
{
113+
MessageWriter.RevertBlockBuffer();
114+
throw;
115+
}
116+
finally
117+
{
118+
if (lease is not null) ArrayPool<byte>.Shared.Return(lease);
119+
MessageWriter.ReleaseBlockBuffer(actual);
120+
}
121+
}
122+
123+
private ICollection<object> Fixup(ICollection<object>? args)
124+
{
125+
if (_keyPrefix is { Length: > 0 } && args is { } && args.Any(x => x is RedisKey))
126+
{
127+
object[] copy = new object[args.Count];
128+
int i = 0;
129+
foreach (object value in args)
130+
{
131+
if (value is RedisKey key)
132+
{
133+
copy[i++] = RedisKey.WithPrefix(_keyPrefix, key);
134+
}
135+
else
136+
{
137+
copy[i++] = value;
138+
}
139+
}
140+
141+
return copy;
142+
}
143+
144+
return args ?? [];
145+
}
146+
147+
/// <summary>
148+
/// Write a RESP frame from a command and set of arguments, and allow a callback to validate
149+
/// the RESP content.
150+
/// </summary>
151+
public void ValidateResp(string expected, string command, params ICollection<object> args)
152+
{
153+
var msg = new RedisDatabase.ExecuteMessage(CommandMap, 0, CommandFlags.None, command, Fixup(args));
154+
var writer = new MessageWriter(ChannelPrefix, CommandMap, MessageWriter.BlockBuffer);
155+
ReadOnlyMemory<byte> payload = default;
156+
char[]? lease = null;
157+
try
158+
{
159+
msg.WriteTo(writer);
160+
payload = MessageWriter.FlushBlockBuffer();
161+
lease = ArrayPool<char>.Shared.Rent(Encoding.UTF8.GetMaxCharCount(payload.Length));
162+
var chars = Encoding.UTF8.GetChars(payload.Span, lease.AsSpan());
163+
var actual = lease.AsSpan(0, chars);
164+
if (!actual.SequenceEqual(expected))
165+
{
166+
OnValidateFail(expected, actual.ToString());
167+
}
168+
}
169+
catch
170+
{
171+
MessageWriter.RevertBlockBuffer();
172+
throw;
173+
}
174+
finally
175+
{
176+
if (lease is not null) ArrayPool<char>.Shared.Return(lease);
177+
MessageWriter.ReleaseBlockBuffer(payload);
178+
}
179+
}
180+
181+
/// <summary>
182+
/// A callback with a payload buffer.
183+
/// </summary>
184+
public delegate void BufferValidator(scoped ReadOnlySpan<byte> buffer);
185+
186+
/// <summary>
187+
/// Deserialize a RESP frame as a <see cref="RedisResult"/>.
188+
/// </summary>
189+
public RedisResult Read(ReadOnlySpan<byte> value)
190+
{
191+
var reader = new RespReader(value);
192+
if (!RedisResult.TryCreate(null, ref reader, out var result))
193+
{
194+
throw new ArgumentException(nameof(value));
195+
}
196+
return result;
197+
}
198+
199+
/// <summary>
200+
/// Convenience handler for comparing span fragments, typically used with "Assert.Equal" or similar
201+
/// as the handler.
202+
/// </summary>
203+
public static void AssertEqual(
204+
ReadOnlySpan<byte> expected,
205+
ReadOnlySpan<byte> actual,
206+
Action<ReadOnlyMemory<byte>, ReadOnlyMemory<byte>> handler)
207+
{
208+
if (!expected.SequenceEqual(actual)) Fault(expected, actual, handler);
209+
static void Fault(
210+
ReadOnlySpan<byte> expected,
211+
ReadOnlySpan<byte> actual,
212+
Action<ReadOnlyMemory<byte>, ReadOnlyMemory<byte>> handler)
213+
{
214+
var lease = ArrayPool<byte>.Shared.Rent(expected.Length + actual.Length);
215+
try
216+
{
217+
var leaseMemory = lease.AsMemory();
218+
var x = leaseMemory.Slice(0, expected.Length);
219+
var y = leaseMemory.Slice(expected.Length, actual.Length);
220+
expected.CopyTo(x.Span);
221+
actual.CopyTo(y.Span);
222+
handler(x, y);
223+
}
224+
finally
225+
{
226+
ArrayPool<byte>.Shared.Return(lease);
227+
}
228+
}
229+
}
230+
231+
/// <summary>
232+
/// Convenience handler for comparing span fragments, typically used with "Assert.Equal" or similar
233+
/// as the handler.
234+
/// </summary>
235+
public static void AssertEqual(
236+
string expected,
237+
ReadOnlySpan<byte> actual,
238+
Action<string, string> handler)
239+
{
240+
var lease = ArrayPool<byte>.Shared.Rent(Encoding.UTF8.GetMaxByteCount(expected.Length));
241+
try
242+
{
243+
var bytes = Encoding.UTF8.GetBytes(expected.AsSpan(), lease.AsSpan());
244+
var span = lease.AsSpan(0, bytes);
245+
if (!span.SequenceEqual(actual)) handler(expected, Encoding.UTF8.GetString(span));
246+
}
247+
finally
248+
{
249+
ArrayPool<byte>.Shared.Return(lease);
250+
}
251+
}
252+
253+
/// <summary>
254+
/// Verify that the routing of a command matches the intent.
255+
/// </summary>
256+
public void ValidateRouting(in RedisKey expected, params ICollection<object> args)
257+
{
258+
var expectedWithPrefix = RedisKey.WithPrefix(_keyPrefix, expected);
259+
var actual = ServerSelectionStrategy.NoSlot;
260+
261+
RedisKey last = RedisKey.Null;
262+
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
263+
if (args is not null)
264+
{
265+
foreach (var arg in args)
266+
{
267+
if (arg is RedisKey key)
268+
{
269+
last = RedisKey.WithPrefix(_keyPrefix, key);
270+
var slot = ServerSelectionStrategy.GetHashSlot(last);
271+
actual = ServerSelectionStrategy.CombineSlot(actual, slot);
272+
}
273+
}
274+
}
275+
276+
if (ServerSelectionStrategy.GetHashSlot(expectedWithPrefix) != actual) OnValidateFail(expectedWithPrefix, last);
277+
}
278+
}

0 commit comments

Comments
 (0)