Skip to content
Open
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
6 changes: 3 additions & 3 deletions DockerComposeFixture.Tests/DockerFixtureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
Thread.Sleep(100);
logger.OnNext("foo");
logger.OnNext(successText);
task.Wait();

Check warning on line 91 in DockerComposeFixture.Tests/DockerFixtureTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not use blocking task operations, as they can cause deadlocks. Use an async test method and await instead. (https://xunit.net/xunit.analyzers/rules/xUnit1031)

Check warning on line 91 in DockerComposeFixture.Tests/DockerFixtureTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not use blocking task operations, as they can cause deadlocks. Use an async test method and await instead. (https://xunit.net/xunit.analyzers/rules/xUnit1031)

compose.Verify(c => c.Init(It.IsAny<string>(), "up", "down"), Times.Once);
compose.Verify(c => c.Up(), Times.Once);
Expand Down Expand Up @@ -125,7 +125,7 @@
[Fact]
public void Init_MonitorsServices_WhenTheyStartSlowly()
{
Stopwatch stopwatch = new Stopwatch();
var stopwatch = new Stopwatch();
var compose = new Mock<IDockerCompose>();
compose.Setup(c => c.PauseMs).Returns(100);
compose.Setup(c => c.Up())
Expand Down Expand Up @@ -158,7 +158,7 @@
{
var compose = new Mock<IDockerCompose>();
compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec);
bool firstTime = true;
var firstTime = true;
compose.Setup(c => c.PsWithJsonFormat())
.Returns(() =>
{
Expand Down Expand Up @@ -191,7 +191,7 @@
compose.Setup(c => c.PsWithJsonFormat())
.Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" });

string fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Assert.Throws<ArgumentException>(() =>
new DockerFixture(null).Init(new[] { fileDoesntExist }, "up", "down", 120, null, compose.Object));
}
Expand Down
2 changes: 2 additions & 0 deletions DockerComposeFixture.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public async Task EchoServer_SaysHello_WhenCalled()
Assert.Contains("hello world", response);
}



public void Dispose()
{
File.Delete(this.dockerComposeFile);
Expand Down
84 changes: 84 additions & 0 deletions DockerComposeFixture.Tests/RequiredPortsIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using DockerComposeFixture.Exceptions;
using Xunit;

namespace DockerComposeFixture.Tests;

public class RequiredPortsIntegrationTests : IClassFixture<DockerFixture>, IDisposable
{
private readonly DockerFixture dockerFixture;
private readonly string dockerComposeFile;

private const string DockerCompose = @"
version: '3.4'
services:
echo_server:
image: hashicorp/http-echo
ports:
- 12871:8080
command: -listen=:8080 -text=""hello world""
";

public RequiredPortsIntegrationTests(DockerFixture dockerFixture)
{
this.dockerFixture = dockerFixture;
this.dockerComposeFile = Path.GetTempFileName();
File.WriteAllText(this.dockerComposeFile, DockerCompose);
}

[Fact]
public void Ports_DetermineUtilizedPorts_ReturnsThoseInUse()
{
// Arrange
const ushort portToOccupy = 12871;
var ipEndPoint = new IPEndPoint(IPAddress.Loopback, portToOccupy);
using Socket listener = new(
ipEndPoint.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
listener.Bind(ipEndPoint);
listener.Listen();

// Act
var usedPorts = Ports.DetermineUtilizedPorts([12870, 12871, 12872]);

// Assert
Assert.Single(usedPorts, p => p == 12871);
}

[Fact]
public void GivenRequiredPortInUse_ThenExceptionIsThrownWithPortNumber()
{
// Arrange
const ushort portToOccupy = 12871;
var ipEndPoint = new IPEndPoint(IPAddress.Loopback, portToOccupy);
using Socket listener = new(
ipEndPoint.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
listener.Bind(ipEndPoint);
listener.Listen();

// Act & Assert
var thrownException = Assert.Throws<PortsUnavailableException>(() =>
{
dockerFixture.InitOnce(() => new DockerFixtureOptions
{
DockerComposeFiles = new[] { this.dockerComposeFile },
CustomUpTest = output => output.Any(l => l.Contains("server is listening")),
RequiredPorts = new ushort[] { portToOccupy }
});
});

Assert.Equivalent(new ushort[] { portToOccupy }, thrownException.Ports);
}

public void Dispose()
{
File.Delete(this.dockerComposeFile);
}
}
2 changes: 1 addition & 1 deletion DockerComposeFixture.Tests/Utils/ObservableCounter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public IDisposable Subscribe(IObserver<string> observer)

public void Count(int min = 1, int max = 10, int delay = 10)
{
for (int i = min; i <= max; i++)
for (var i = min; i <= max; i++)
{
this.observalbes.ForEach(o => o.OnNext(i.ToString()));
Thread.Sleep(delay);
Expand Down
6 changes: 3 additions & 3 deletions DockerComposeFixture/DockerComposeFixture.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Version>1.2.2</Version>
<Version>1.3.0</Version>
<IsTestProject>false</IsTestProject>
<Authors>Joe Shearn</Authors>
<Product>Docker Compose Fixture</Product>
Expand All @@ -13,8 +13,8 @@
<PackageLicenseUrl>https://github.com/devjoes/DockerComposeFixture/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/devjoes/DockerComposeFixture</RepositoryUrl>
<PackageTags>docker docker-compose xunit</PackageTags>
<PackageReleaseNotes>Do not throw null reference if fixture is disposed of without being initialized.</PackageReleaseNotes>
<AssemblyVersion>1.2.2.0</AssemblyVersion>
<PackageReleaseNotes>Add RequiredPorts checking functionality.</PackageReleaseNotes>
<AssemblyVersion>1.3.0.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion DockerComposeFixture/DockerComposeFixture.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<metadata minClientVersion="2.12">
<id>dockercomposefixture</id>
<title>docker-compose Fixture</title>
<version>1.2.2</version>
<version>1.3.0</version>
<authors>Joe Shearn</authors>
<owners>Joe Shearn</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
Expand Down
61 changes: 43 additions & 18 deletions DockerComposeFixture/DockerFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class DockerFixture : IDisposable
private ILogger[] loggers;
private int startupTimeoutSecs;
private readonly IMessageSink output;
private ushort[] requiredPorts;

public DockerFixture(IMessageSink output)
{
Expand All @@ -31,7 +32,7 @@ public DockerFixture(IMessageSink output)
/// Initialize docker compose services from file(s) but only once.
/// If you call this multiple times on the same DockerFixture then it will be ignored.
/// </summary>
/// <param name="setupOptions">Options that control how docker-compose is executed.</param>
/// <param name="setupOptions">Options that control how docker compose is executed.</param>
public void InitOnce(Func<IDockerFixtureOptions> setupOptions)
{
InitOnce(setupOptions, null);
Expand All @@ -41,7 +42,7 @@ public void InitOnce(Func<IDockerFixtureOptions> setupOptions)
/// Initialize docker compose services from file(s) but only once.
/// If you call this multiple times on the same DockerFixture then it will be ignored.
/// </summary>
/// <param name="setupOptions">Options that control how docker-compose is executed.</param>
/// <param name="setupOptions">Options that control how docker compose is executed.</param>
/// <param name="dockerCompose"></param>
public void InitOnce(Func<IDockerFixtureOptions> setupOptions, IDockerCompose dockerCompose)
{
Expand All @@ -56,7 +57,7 @@ public void InitOnce(Func<IDockerFixtureOptions> setupOptions, IDockerCompose do
/// <summary>
/// Initialize docker compose services from file(s).
/// </summary>
/// <param name="setupOptions">Options that control how docker-compose is executed</param>
/// <param name="setupOptions">Options that control how docker compose is executed</param>
public void Init(Func<IDockerFixtureOptions> setupOptions)
{
Init(setupOptions, null);
Expand All @@ -65,18 +66,25 @@ public void Init(Func<IDockerFixtureOptions> setupOptions)
/// <summary>
/// Initialize docker compose services from file(s).
/// </summary>
/// <param name="setupOptions">Options that control how docker-compose is executed</param>
/// <param name="setupOptions">Options that control how docker compose is executed</param>
/// <param name="compose"></param>
public void Init(Func<IDockerFixtureOptions> setupOptions, IDockerCompose compose)
{
var options = setupOptions();
options.Validate();
string logFile = options.DebugLog
var logFile = options.DebugLog
? Path.Combine(Path.GetTempPath(), $"docker-compose-{DateTime.Now.Ticks}.log")
: null;

this.Init(options.DockerComposeFiles, options.DockerComposeUpArgs, options.DockerComposeDownArgs,
options.StartupTimeoutSecs, options.CustomUpTest, compose, this.GetLoggers(logFile).ToArray());
this.Init(
options.DockerComposeFiles,
options.DockerComposeUpArgs,
options.DockerComposeDownArgs,
options.StartupTimeoutSecs,
options.CustomUpTest,
compose,
this.GetLoggers(logFile).ToArray(),
options.RequiredPorts);
}

private IEnumerable<ILogger> GetLoggers(string file)
Expand All @@ -97,22 +105,24 @@ private IEnumerable<ILogger> GetLoggers(string file)
/// Initialize docker compose services from file(s).
/// </summary>
/// <param name="dockerComposeFiles">Array of docker compose files</param>
/// <param name="dockerComposeUpArgs">Arguments to append after 'docker-compose -f file.yml up'</param>
/// <param name="dockerComposeDownArgs">Arguments to append after 'docker-compose -f file.yml down'</param>
/// <param name="dockerComposeUpArgs">Arguments to append after 'docker compose -f file.yml up'</param>
/// <param name="dockerComposeDownArgs">Arguments to append after 'docker compose -f file.yml down'</param>
/// <param name="startupTimeoutSecs">How long to wait for the application to start before giving up</param>
/// <param name="customUpTest">Checks whether the docker-compose services have come up correctly based upon the output of docker-compose</param>
/// <param name="customUpTest">Checks whether the docker compose services have come up correctly based upon the output of docker compose</param>
/// <param name="dockerCompose"></param>
/// <param name="logger"></param>
/// <param name="requiredPorts">Checks that these ports are available on the host network (not in use by other processes)</param>
public void Init(string[] dockerComposeFiles, string dockerComposeUpArgs, string dockerComposeDownArgs,
int startupTimeoutSecs, Func<string[], bool> customUpTest = null,
IDockerCompose dockerCompose = null, ILogger[] logger = null)
IDockerCompose dockerCompose = null, ILogger[] logger = null, ushort[] requiredPorts = null)
{
this.loggers = logger ?? GetLoggers(null).ToArray();

var dockerComposeFilePaths = dockerComposeFiles.Select(this.GetComposeFilePath);
this.dockerCompose = dockerCompose ?? new DockerCompose(this.loggers);
this.customUpTest = customUpTest;
this.startupTimeoutSecs = startupTimeoutSecs;
this.requiredPorts = requiredPorts;

this.dockerCompose.Init(
string.Join(" ",
Expand All @@ -129,7 +139,7 @@ private string GetComposeFilePath(string file)
return file;
}

DirectoryInfo curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
var curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
if (File.Exists(Path.Combine(curDir.FullName, file)))
{
return Path.Combine(curDir.FullName, file);
Expand All @@ -139,7 +149,7 @@ private string GetComposeFilePath(string file)
{
while (curDir != null)
{
string curFile = Path.Combine(curDir.FullName, file);
var curFile = Path.Combine(curDir.FullName, file);
if (File.Exists(curFile))
{
return curFile;
Expand Down Expand Up @@ -169,7 +179,7 @@ public static async Task Kill(string applicationName, bool killEverything = fals
/// <returns></returns>
public static async Task Kill(Regex filterRx, bool killEverything = false)
{
Process ps = Process.Start(new ProcessStartInfo("docker", "ps")
var ps = Process.Start(new ProcessStartInfo("docker", "ps")
{
UseShellExecute = false,
RedirectStandardOutput = true
Expand All @@ -187,7 +197,6 @@ public static async Task Kill(Regex filterRx, bool killEverything = false)
{
Process.Start("docker", $"kill {id}").WaitForExit();
}

}

public virtual void Dispose()
Expand All @@ -203,14 +212,21 @@ private void Start()
this.Stop();
}

if (requiredPorts != null && requiredPorts.Length > 0)
{
this.loggers.Log("---- checking for port availability ----");
this.CheckForRequiredPorts();
this.loggers.Log("---- all required host ports are available ----");
}

this.loggers.Log("---- starting docker services ----");
var upTask = this.dockerCompose.Up();

for (int i = 0; i < this.startupTimeoutSecs; i++)
for (var i = 0; i < this.startupTimeoutSecs; i++)
{
if (upTask.IsCompleted)
{
this.loggers.Log("docker-compose exited prematurely");
this.loggers.Log("docker compose exited prematurely");
break;
}
this.loggers.Log($"---- checking docker services ({i + 1}/{this.startupTimeoutSecs}) ----");
Expand All @@ -235,7 +251,16 @@ private void Start()
}
throw new DockerComposeException(this.loggers.GetLoggedLines());
}


private void CheckForRequiredPorts()
{
var usedPorts = Ports.DetermineUtilizedPorts(requiredPorts);
if (usedPorts.Any())
{
throw new PortsUnavailableException(this.loggers.GetLoggedLines(), usedPorts);
}
}

private (bool hasContainers, bool containersAreUp) CheckIfRunning()
{
var lines = dockerCompose.PsWithJsonFormat()
Expand Down
23 changes: 16 additions & 7 deletions DockerComposeFixture/DockerFixtureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
namespace DockerComposeFixture
{
/// <summary>
/// Options that control how docker-compose is executed
/// Options that control how docker compose is executed
/// </summary>
public class DockerFixtureOptions : IDockerFixtureOptions
{
/// <summary>
/// Checks whether the docker-compose services have come up correctly based upon the output of docker-compose
/// An array of ports required to be available on the host in order for the docker compose services
/// to start. Provides a fail-fast check along with aiding developers to easier debug issues related
/// to running other programs locally with clashing ports.
/// If null or empty - no ports are checked.
/// If any required ports are reserved by other processes - throws an 'PortsUnavailableException'.
/// </summary>
public ushort[] RequiredPorts { get; set; }

/// <summary>
/// Checks whether the docker compose services have come up correctly based upon the output of docker compose
/// </summary>
public Func<string[], bool> CustomUpTest { get; set; }

Expand All @@ -19,17 +28,17 @@ public class DockerFixtureOptions : IDockerFixtureOptions
/// </summary>
public string[] DockerComposeFiles { get; set; } = new[] { "docker-compose.yml" };
/// <summary>
/// When true this logs docker-compose output to %temp%\docker-compose-*.log
/// When true this logs docker compose output to %temp%\docker-compose-*.log
/// </summary>
public bool DebugLog { get; set; }
/// <summary>
/// Arguments to append after 'docker-compose -f file.yml up'
/// Default is 'docker-compose -f file.yml up' you can append '--build' if you want it to always build
/// Arguments to append after 'docker compose -f file.yml up'
/// Default is 'docker compose -f file.yml up' you can append '--build' if you want it to always build
/// </summary>
public string DockerComposeUpArgs { get; set; } = "";
/// <summary>
/// Arguments to append after 'docker-compose -f file.yml down'
/// Default is 'docker-compose -f file.yml down --remove-orphans' you can add '--rmi all' if you want to guarantee a fresh build on each test
/// Arguments to append after 'docker compose -f file.yml down'
/// Default is 'docker compose -f file.yml down --remove-orphans' you can add '--rmi all' if you want to guarantee a fresh build on each test
/// </summary>
public string DockerComposeDownArgs { get; set; } = "--remove-orphans";

Expand Down
5 changes: 3 additions & 2 deletions DockerComposeFixture/Exceptions/DockerComposeException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

namespace DockerComposeFixture.Exceptions
{
public class DockerComposeException:Exception
public class DockerComposeException : Exception
{
public DockerComposeException(string[] loggedLines):base($"docker-compose failed - see {nameof(DockerComposeOutput)} property")
public DockerComposeException(string[] loggedLines)
: base($"docker compose failed - see {nameof(DockerComposeOutput)} property")
{
this.DockerComposeOutput = loggedLines;
}
Expand Down
Loading
Loading