Skip to content

Commit 117139d

Browse files
committed
chore: Use testcontainers for redis tests
1 parent e5b0096 commit 117139d

File tree

6 files changed

+49
-66
lines changed

6 files changed

+49
-66
lines changed

.github/workflows/ci.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
12+
- name: Setup .NET 8
13+
uses: actions/setup-dotnet@v4
14+
with:
15+
dotnet-version: 8.0.x
16+
17+
- name: Build
18+
run: dotnet build src
19+
20+
- name: Redis Tests
21+
run: dotnet test src --filter Name~Redis
22+

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
2323
<PackageVersion Include="Moq" Version="4.20.70" />
2424
<PackageVersion Include="System.Threading.AccessControl" Version="8.0.0" Condition="'$(TargetFramework)' != 'net462'" />
25+
<PackageVersion Include="Testcontainers.Redis" Version="3.8.0" />
2526
<PackageVersion Include="ZooKeeperNetEx" Version="3.4.12.4" />
2627
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
2728
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'net462'" />

src/DistributedLock.Tests/DistributedLock.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<PackageReference Include="MedallionShell.StrongName" />
2828
<PackageReference Include="System.Data.SqlClient" />
2929
<PackageReference Include="Moq" />
30+
<PackageReference Include="Testcontainers.Redis" />
3031
</ItemGroup>
3132

3233
<ItemGroup>
Lines changed: 23 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using Medallion.Shell;
2-
using StackExchange.Redis;
1+
using StackExchange.Redis;
2+
using Testcontainers.Redis;
33

44
namespace Medallion.Threading.Tests.Redis;
55

@@ -8,33 +8,29 @@ internal class RedisServer
88
// redis default is 6379, so go one above that
99
private static readonly int MinDynamicPort = RedisPorts.DefaultPorts.Max() + 1, MaxDynamicPort = MinDynamicPort + 100;
1010

11-
// it's important for this to be lazy because it doesn't work when running on Linux
12-
private static readonly Lazy<string> WslPath = new(
13-
() => Directory.GetDirectories(@"C:\Windows\WinSxS")
14-
.Select(d => Path.Combine(d, "wsl.exe"))
15-
.Where(File.Exists)
16-
.OrderByDescending(File.GetCreationTimeUtc)
17-
.First()
18-
);
11+
public static async Task DisposeAsync()
12+
{
13+
foreach (var container in RedisContainers)
14+
{
15+
await container.StopAsync();
16+
}
17+
}
1918

20-
private static readonly Dictionary<int, RedisServer> ActiveServersByPort = [];
19+
private static readonly List<RedisContainer> RedisContainers = [];
2120
private static readonly RedisServer[] DefaultServers = new RedisServer[RedisPorts.DefaultPorts.Count];
2221

23-
private readonly Command _command;
22+
private readonly RedisContainer _redis;
2423

25-
public RedisServer(bool allowAdmin = false) : this(null, allowAdmin) { }
26-
27-
private RedisServer(int? port, bool allowAdmin)
24+
public RedisServer(bool allowAdmin = false)
2825
{
29-
lock (ActiveServersByPort)
30-
{
31-
this.Port = port ?? Enumerable.Range(MinDynamicPort, count: MaxDynamicPort - MinDynamicPort + 1)
32-
.First(p => !ActiveServersByPort.ContainsKey(p));
33-
this._command = Command.Run(WslPath.Value, ["redis-server", "--port", this.Port], options: o => o.StartInfo(si => si.RedirectStandardInput = false))
34-
.RedirectTo(Console.Out)
35-
.RedirectStandardErrorTo(Console.Error);
36-
ActiveServersByPort.Add(this.Port, this);
37-
}
26+
_redis = new RedisBuilder()
27+
.WithPortBinding(MinDynamicPort + RedisContainers.Count)
28+
.Build();
29+
_redis.StartAsync().Wait();
30+
RedisContainers.Add(_redis);
31+
32+
this.Port = _redis.GetMappedPublicPort(RedisBuilder.RedisPort);
33+
3834
this.Multiplexer = ConnectionMultiplexer.Connect($"localhost:{this.Port},abortConnect=false{(allowAdmin ? ",allowAdmin=true" : string.Empty)}");
3935
// Clean the db to ensure it is empty. Running an arbitrary command also ensures that
4036
// the db successfully spun up before we proceed (Connect seemingly can complete before that happens).
@@ -43,49 +39,16 @@ private RedisServer(int? port, bool allowAdmin)
4339
this.Multiplexer.GetDatabase().Execute("flushall", Array.Empty<object>(), CommandFlags.DemandMaster);
4440
}
4541

46-
public int ProcessId => this._command.ProcessId;
4742
public int Port { get; }
4843
public ConnectionMultiplexer Multiplexer { get; }
4944

45+
public void Dispose() => _redis.DisposeAsync().GetAwaiter().GetResult();
46+
5047
public static RedisServer GetDefaultServer(int index)
5148
{
5249
lock (DefaultServers)
5350
{
54-
return DefaultServers[index] ??= new RedisServer(RedisPorts.DefaultPorts[index], allowAdmin: false);
55-
}
56-
}
57-
58-
public static void DisposeAll()
59-
{
60-
lock (ActiveServersByPort)
61-
{
62-
var shutdownTasks = ActiveServersByPort.Values
63-
.Select(async server =>
64-
{
65-
// When testing the case of a server outage, we'll have manually shut down some servers.
66-
// In that case, we shouldn't attempt to connect to them since that will fail.
67-
var isConnected = server.Multiplexer.GetServers().Any(s => s.IsConnected);
68-
server.Multiplexer.Dispose();
69-
try
70-
{
71-
if (isConnected)
72-
{
73-
using var adminMultiplexer = await ConnectionMultiplexer.ConnectAsync($"localhost:{server.Port},allowAdmin=true");
74-
adminMultiplexer.GetServer("localhost", server.Port).Shutdown(ShutdownMode.Never);
75-
}
76-
}
77-
finally
78-
{
79-
if (!await server._command.Task.TryWaitAsync(TimeSpan.FromSeconds(5)))
80-
{
81-
server._command.Kill();
82-
throw new InvalidOperationException("Forced to kill Redis server");
83-
}
84-
}
85-
})
86-
.ToArray();
87-
ActiveServersByPort.Clear();
88-
Task.WaitAll(shutdownTasks);
51+
return DefaultServers[index] ??= new RedisServer(allowAdmin: false);
8952
}
9053
}
9154
}

src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ namespace Medallion.Threading.Tests.Redis;
55
[SetUpFixture]
66
public class RedisSetUpFixture
77
{
8-
[OneTimeSetUp]
9-
public void OneTimeSetUp() { }
10-
118
[OneTimeTearDown]
12-
public void OneTimeTearDown() => RedisServer.DisposeAll();
9+
public Task OneTimeTearDown() => RedisServer.DisposeAsync();
1310
}

src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ static TestingRedis2x1DatabaseProvider()
4949
{
5050
var server = new RedisServer(allowAdmin: true);
5151
DeadDatabase = server.Multiplexer.GetDatabase();
52-
using var process = Process.GetProcessById(server.ProcessId);
5352
server.Multiplexer.GetServer($"localhost:{server.Port}").Shutdown(ShutdownMode.Never);
54-
Assert.That(process.WaitForExit(5000), Is.True);
53+
server.Dispose();
5554
}
5655

5756
public TestingRedis2x1DatabaseProvider()

0 commit comments

Comments
 (0)