diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..1e614b19 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -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 + \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a7d2f366..01d076f4 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,6 +22,7 @@ + diff --git a/src/DistributedLock.Tests/DistributedLock.Tests.csproj b/src/DistributedLock.Tests/DistributedLock.Tests.csproj index be82219e..6710cce8 100644 --- a/src/DistributedLock.Tests/DistributedLock.Tests.csproj +++ b/src/DistributedLock.Tests/DistributedLock.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs b/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs index d524205a..3d2a32ea 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/RedisServer.cs @@ -1,5 +1,5 @@ -using Medallion.Shell; -using StackExchange.Redis; +using StackExchange.Redis; +using Testcontainers.Redis; namespace Medallion.Threading.Tests.Redis; @@ -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 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 ActiveServersByPort = []; + private static readonly List 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). @@ -43,49 +39,16 @@ private RedisServer(int? port, bool allowAdmin) this.Multiplexer.GetDatabase().Execute("flushall", Array.Empty(), 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); } } } diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs b/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs index 0226708a..7943b697 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/RedisSetUpFixture.cs @@ -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(); } diff --git a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs index 875ec6c5..fc90c60e 100644 --- a/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs +++ b/src/DistributedLock.Tests/Infrastructure/Redis/TestingRedisDatabaseProvider.cs @@ -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()