Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: use testcontainers.redis for integration tests #208

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Build
run: dotnet build src

- name: Redis Tests
run: dotnet test src --filter Name~Redis

1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="System.Threading.AccessControl" Version="8.0.0" Condition="'$(TargetFramework)' != 'net462'" />
<PackageVersion Include="Testcontainers.Redis" Version="3.8.0" />
<PackageVersion Include="ZooKeeperNetEx" Version="3.4.12.4" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'net462'" />
Expand Down
1 change: 1 addition & 0 deletions src/DistributedLock.Tests/DistributedLock.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageReference Include="MedallionShell.StrongName" />
<PackageReference Include="System.Data.SqlClient" />
<PackageReference Include="Moq" />
<PackageReference Include="Testcontainers.Redis" />
</ItemGroup>

<ItemGroup>
Expand Down
83 changes: 23 additions & 60 deletions src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Medallion.Shell;
using StackExchange.Redis;
using StackExchange.Redis;
using Testcontainers.Redis;

namespace Medallion.Threading.Tests.Redis;

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

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

private static readonly Dictionary<int, RedisServer> ActiveServersByPort = [];
private static readonly List<RedisContainer> RedisContainers = [];
private static readonly RedisServer[] DefaultServers = new RedisServer[RedisPorts.DefaultPorts.Count];

private readonly Command _command;
private readonly RedisContainer _redis;

public RedisServer(bool allowAdmin = false) : this(null, allowAdmin) { }

private RedisServer(int? port, bool allowAdmin)
public RedisServer(bool allowAdmin = false)
{
lock (ActiveServersByPort)
{
this.Port = port ?? Enumerable.Range(MinDynamicPort, count: MaxDynamicPort - MinDynamicPort + 1)
.First(p => !ActiveServersByPort.ContainsKey(p));
this._command = Command.Run(WslPath.Value, ["redis-server", "--port", this.Port], options: o => o.StartInfo(si => si.RedirectStandardInput = false))
.RedirectTo(Console.Out)
.RedirectStandardErrorTo(Console.Error);
ActiveServersByPort.Add(this.Port, this);
}
_redis = new RedisBuilder()
.WithPortBinding(MinDynamicPort + RedisContainers.Count)
.Build();
_redis.StartAsync().Wait();
RedisContainers.Add(_redis);

this.Port = _redis.GetMappedPublicPort(RedisBuilder.RedisPort);

this.Multiplexer = ConnectionMultiplexer.Connect($"localhost:{this.Port},abortConnect=false{(allowAdmin ? ",allowAdmin=true" : string.Empty)}");
// Clean the db to ensure it is empty. Running an arbitrary command also ensures that
// the db successfully spun up before we proceed (Connect seemingly can complete before that happens).
Expand All @@ -43,49 +39,16 @@ private RedisServer(int? port, bool allowAdmin)
this.Multiplexer.GetDatabase().Execute("flushall", Array.Empty<object>(), CommandFlags.DemandMaster);
}

public int ProcessId => this._command.ProcessId;
public int Port { get; }
public ConnectionMultiplexer Multiplexer { get; }

public void Dispose() => _redis.DisposeAsync().GetAwaiter().GetResult();

public static RedisServer GetDefaultServer(int index)
{
lock (DefaultServers)
{
return DefaultServers[index] ??= new RedisServer(RedisPorts.DefaultPorts[index], allowAdmin: false);
}
}

public static void DisposeAll()
{
lock (ActiveServersByPort)
{
var shutdownTasks = ActiveServersByPort.Values
.Select(async server =>
{
// When testing the case of a server outage, we'll have manually shut down some servers.
// In that case, we shouldn't attempt to connect to them since that will fail.
var isConnected = server.Multiplexer.GetServers().Any(s => s.IsConnected);
server.Multiplexer.Dispose();
try
{
if (isConnected)
{
using var adminMultiplexer = await ConnectionMultiplexer.ConnectAsync($"localhost:{server.Port},allowAdmin=true");
adminMultiplexer.GetServer("localhost", server.Port).Shutdown(ShutdownMode.Never);
}
}
finally
{
if (!await server._command.Task.TryWaitAsync(TimeSpan.FromSeconds(5)))
{
server._command.Kill();
throw new InvalidOperationException("Forced to kill Redis server");
}
}
})
.ToArray();
ActiveServersByPort.Clear();
Task.WaitAll(shutdownTasks);
return DefaultServers[index] ??= new RedisServer(allowAdmin: false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ namespace Medallion.Threading.Tests.Redis;
[SetUpFixture]
public class RedisSetUpFixture
{
[OneTimeSetUp]
public void OneTimeSetUp() { }

[OneTimeTearDown]
public void OneTimeTearDown() => RedisServer.DisposeAll();
public Task OneTimeTearDown() => RedisServer.DisposeAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ static TestingRedis2x1DatabaseProvider()
{
var server = new RedisServer(allowAdmin: true);
DeadDatabase = server.Multiplexer.GetDatabase();
using var process = Process.GetProcessById(server.ProcessId);
server.Multiplexer.GetServer($"localhost:{server.Port}").Shutdown(ShutdownMode.Never);
Assert.That(process.WaitForExit(5000), Is.True);
server.Dispose();
}

public TestingRedis2x1DatabaseProvider()
Expand Down