Skip to content

Commit cb421ab

Browse files
Merge pull request #22 from devjoes/add-requiredports-option
Adds 'requiredPorts' functionality
2 parents eea71d7 + 06f55bf commit cb421ab

12 files changed

+202
-35
lines changed

DockerComposeFixture.Tests/DockerFixtureTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void Init_Throws_IfTestIsNeverTrue()
125125
[Fact]
126126
public void Init_MonitorsServices_WhenTheyStartSlowly()
127127
{
128-
Stopwatch stopwatch = new Stopwatch();
128+
var stopwatch = new Stopwatch();
129129
var compose = new Mock<IDockerCompose>();
130130
compose.Setup(c => c.PauseMs).Returns(100);
131131
compose.Setup(c => c.Up())
@@ -158,7 +158,7 @@ public void Init_Throws_WhenServicesFailToStart()
158158
{
159159
var compose = new Mock<IDockerCompose>();
160160
compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec);
161-
bool firstTime = true;
161+
var firstTime = true;
162162
compose.Setup(c => c.PsWithJsonFormat())
163163
.Returns(() =>
164164
{
@@ -191,7 +191,7 @@ public void Init_Throws_WhenYmlFileDoesntExist()
191191
compose.Setup(c => c.PsWithJsonFormat())
192192
.Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" });
193193

194-
string fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
194+
var fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
195195
Assert.Throws<ArgumentException>(() =>
196196
new DockerFixture(null).Init(new[] { fileDoesntExist }, "up", "down", 120, null, compose.Object));
197197
}

DockerComposeFixture.Tests/IntegrationTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public async Task EchoServer_SaysHello_WhenCalled()
4242
Assert.Contains("hello world", response);
4343
}
4444

45+
46+
4547
public void Dispose()
4648
{
4749
File.Delete(this.dockerComposeFile);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Sockets;
6+
using DockerComposeFixture.Exceptions;
7+
using Xunit;
8+
9+
namespace DockerComposeFixture.Tests;
10+
11+
public class RequiredPortsIntegrationTests : IClassFixture<DockerFixture>, IDisposable
12+
{
13+
private readonly DockerFixture dockerFixture;
14+
private readonly string dockerComposeFile;
15+
16+
private const string DockerCompose = @"
17+
version: '3.4'
18+
services:
19+
echo_server:
20+
image: hashicorp/http-echo
21+
ports:
22+
- 12871:8080
23+
command: -listen=:8080 -text=""hello world""
24+
";
25+
26+
public RequiredPortsIntegrationTests(DockerFixture dockerFixture)
27+
{
28+
this.dockerFixture = dockerFixture;
29+
this.dockerComposeFile = Path.GetTempFileName();
30+
File.WriteAllText(this.dockerComposeFile, DockerCompose);
31+
}
32+
33+
[Fact]
34+
public void Ports_DetermineUtilizedPorts_ReturnsThoseInUse()
35+
{
36+
// Arrange
37+
const ushort portToOccupy = 12871;
38+
var ipEndPoint = new IPEndPoint(IPAddress.Loopback, portToOccupy);
39+
using Socket listener = new(
40+
ipEndPoint.AddressFamily,
41+
SocketType.Stream,
42+
ProtocolType.Tcp);
43+
listener.Bind(ipEndPoint);
44+
listener.Listen();
45+
46+
// Act
47+
var usedPorts = Ports.DetermineUtilizedPorts([12870, 12871, 12872]);
48+
49+
// Assert
50+
Assert.Single(usedPorts, p => p == 12871);
51+
}
52+
53+
[Fact]
54+
public void GivenRequiredPortInUse_ThenExceptionIsThrownWithPortNumber()
55+
{
56+
// Arrange
57+
const ushort portToOccupy = 12871;
58+
var ipEndPoint = new IPEndPoint(IPAddress.Loopback, portToOccupy);
59+
using Socket listener = new(
60+
ipEndPoint.AddressFamily,
61+
SocketType.Stream,
62+
ProtocolType.Tcp);
63+
listener.Bind(ipEndPoint);
64+
listener.Listen();
65+
66+
// Act & Assert
67+
var thrownException = Assert.Throws<PortsUnavailableException>(() =>
68+
{
69+
dockerFixture.InitOnce(() => new DockerFixtureOptions
70+
{
71+
DockerComposeFiles = new[] { this.dockerComposeFile },
72+
CustomUpTest = output => output.Any(l => l.Contains("server is listening")),
73+
RequiredPorts = new ushort[] { portToOccupy }
74+
});
75+
});
76+
77+
Assert.Equivalent(new ushort[] { portToOccupy }, thrownException.Ports);
78+
}
79+
80+
public void Dispose()
81+
{
82+
File.Delete(this.dockerComposeFile);
83+
}
84+
}

DockerComposeFixture.Tests/Utils/ObservableCounter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public IDisposable Subscribe(IObserver<string> observer)
1616

1717
public void Count(int min = 1, int max = 10, int delay = 10)
1818
{
19-
for (int i = min; i <= max; i++)
19+
for (var i = min; i <= max; i++)
2020
{
2121
this.observalbes.ForEach(o => o.OnNext(i.ToString()));
2222
Thread.Sleep(delay);

DockerComposeFixture/DockerComposeFixture.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

2020
<ItemGroup>

DockerComposeFixture/DockerComposeFixture.nuspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<metadata minClientVersion="2.12">
44
<id>dockercomposefixture</id>
55
<title>docker-compose Fixture</title>
6-
<version>1.2.2</version>
6+
<version>1.3.0</version>
77
<authors>Joe Shearn</authors>
88
<owners>Joe Shearn</owners>
99
<requireLicenseAcceptance>false</requireLicenseAcceptance>

DockerComposeFixture/DockerFixture.cs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class DockerFixture : IDisposable
2121
private ILogger[] loggers;
2222
private int startupTimeoutSecs;
2323
private readonly IMessageSink output;
24+
private ushort[] requiredPorts;
2425

2526
public DockerFixture(IMessageSink output)
2627
{
@@ -31,7 +32,7 @@ public DockerFixture(IMessageSink output)
3132
/// Initialize docker compose services from file(s) but only once.
3233
/// If you call this multiple times on the same DockerFixture then it will be ignored.
3334
/// </summary>
34-
/// <param name="setupOptions">Options that control how docker-compose is executed.</param>
35+
/// <param name="setupOptions">Options that control how docker compose is executed.</param>
3536
public void InitOnce(Func<IDockerFixtureOptions> setupOptions)
3637
{
3738
InitOnce(setupOptions, null);
@@ -41,7 +42,7 @@ public void InitOnce(Func<IDockerFixtureOptions> setupOptions)
4142
/// Initialize docker compose services from file(s) but only once.
4243
/// If you call this multiple times on the same DockerFixture then it will be ignored.
4344
/// </summary>
44-
/// <param name="setupOptions">Options that control how docker-compose is executed.</param>
45+
/// <param name="setupOptions">Options that control how docker compose is executed.</param>
4546
/// <param name="dockerCompose"></param>
4647
public void InitOnce(Func<IDockerFixtureOptions> setupOptions, IDockerCompose dockerCompose)
4748
{
@@ -56,7 +57,7 @@ public void InitOnce(Func<IDockerFixtureOptions> setupOptions, IDockerCompose do
5657
/// <summary>
5758
/// Initialize docker compose services from file(s).
5859
/// </summary>
59-
/// <param name="setupOptions">Options that control how docker-compose is executed</param>
60+
/// <param name="setupOptions">Options that control how docker compose is executed</param>
6061
public void Init(Func<IDockerFixtureOptions> setupOptions)
6162
{
6263
Init(setupOptions, null);
@@ -65,18 +66,25 @@ public void Init(Func<IDockerFixtureOptions> setupOptions)
6566
/// <summary>
6667
/// Initialize docker compose services from file(s).
6768
/// </summary>
68-
/// <param name="setupOptions">Options that control how docker-compose is executed</param>
69+
/// <param name="setupOptions">Options that control how docker compose is executed</param>
6970
/// <param name="compose"></param>
7071
public void Init(Func<IDockerFixtureOptions> setupOptions, IDockerCompose compose)
7172
{
7273
var options = setupOptions();
7374
options.Validate();
74-
string logFile = options.DebugLog
75+
var logFile = options.DebugLog
7576
? Path.Combine(Path.GetTempPath(), $"docker-compose-{DateTime.Now.Ticks}.log")
7677
: null;
7778

78-
this.Init(options.DockerComposeFiles, options.DockerComposeUpArgs, options.DockerComposeDownArgs,
79-
options.StartupTimeoutSecs, options.CustomUpTest, compose, this.GetLoggers(logFile).ToArray());
79+
this.Init(
80+
options.DockerComposeFiles,
81+
options.DockerComposeUpArgs,
82+
options.DockerComposeDownArgs,
83+
options.StartupTimeoutSecs,
84+
options.CustomUpTest,
85+
compose,
86+
this.GetLoggers(logFile).ToArray(),
87+
options.RequiredPorts);
8088
}
8189

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

112121
var dockerComposeFilePaths = dockerComposeFiles.Select(this.GetComposeFilePath);
113122
this.dockerCompose = dockerCompose ?? new DockerCompose(this.loggers);
114123
this.customUpTest = customUpTest;
115124
this.startupTimeoutSecs = startupTimeoutSecs;
125+
this.requiredPorts = requiredPorts;
116126

117127
this.dockerCompose.Init(
118128
string.Join(" ",
@@ -129,7 +139,7 @@ private string GetComposeFilePath(string file)
129139
return file;
130140
}
131141

132-
DirectoryInfo curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
142+
var curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
133143
if (File.Exists(Path.Combine(curDir.FullName, file)))
134144
{
135145
return Path.Combine(curDir.FullName, file);
@@ -139,7 +149,7 @@ private string GetComposeFilePath(string file)
139149
{
140150
while (curDir != null)
141151
{
142-
string curFile = Path.Combine(curDir.FullName, file);
152+
var curFile = Path.Combine(curDir.FullName, file);
143153
if (File.Exists(curFile))
144154
{
145155
return curFile;
@@ -169,7 +179,7 @@ public static async Task Kill(string applicationName, bool killEverything = fals
169179
/// <returns></returns>
170180
public static async Task Kill(Regex filterRx, bool killEverything = false)
171181
{
172-
Process ps = Process.Start(new ProcessStartInfo("docker", "ps")
182+
var ps = Process.Start(new ProcessStartInfo("docker", "ps")
173183
{
174184
UseShellExecute = false,
175185
RedirectStandardOutput = true
@@ -187,7 +197,6 @@ public static async Task Kill(Regex filterRx, bool killEverything = false)
187197
{
188198
Process.Start("docker", $"kill {id}").WaitForExit();
189199
}
190-
191200
}
192201

193202
public virtual void Dispose()
@@ -203,14 +212,21 @@ private void Start()
203212
this.Stop();
204213
}
205214

215+
if (requiredPorts != null && requiredPorts.Length > 0)
216+
{
217+
this.loggers.Log("---- checking for port availability ----");
218+
this.CheckForRequiredPorts();
219+
this.loggers.Log("---- all required host ports are available ----");
220+
}
221+
206222
this.loggers.Log("---- starting docker services ----");
207223
var upTask = this.dockerCompose.Up();
208224

209-
for (int i = 0; i < this.startupTimeoutSecs; i++)
225+
for (var i = 0; i < this.startupTimeoutSecs; i++)
210226
{
211227
if (upTask.IsCompleted)
212228
{
213-
this.loggers.Log("docker-compose exited prematurely");
229+
this.loggers.Log("docker compose exited prematurely");
214230
break;
215231
}
216232
this.loggers.Log($"---- checking docker services ({i + 1}/{this.startupTimeoutSecs}) ----");
@@ -235,7 +251,16 @@ private void Start()
235251
}
236252
throw new DockerComposeException(this.loggers.GetLoggedLines());
237253
}
238-
254+
255+
private void CheckForRequiredPorts()
256+
{
257+
var usedPorts = Ports.DetermineUtilizedPorts(requiredPorts);
258+
if (usedPorts.Any())
259+
{
260+
throw new PortsUnavailableException(this.loggers.GetLoggedLines(), usedPorts);
261+
}
262+
}
263+
239264
private (bool hasContainers, bool containersAreUp) CheckIfRunning()
240265
{
241266
var lines = dockerCompose.PsWithJsonFormat()

DockerComposeFixture/DockerFixtureOptions.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
namespace DockerComposeFixture
44
{
55
/// <summary>
6-
/// Options that control how docker-compose is executed
6+
/// Options that control how docker compose is executed
77
/// </summary>
88
public class DockerFixtureOptions : IDockerFixtureOptions
99
{
1010
/// <summary>
11-
/// Checks whether the docker-compose services have come up correctly based upon the output of docker-compose
11+
/// An array of ports required to be available on the host in order for the docker compose services
12+
/// to start. Provides a fail-fast check along with aiding developers to easier debug issues related
13+
/// to running other programs locally with clashing ports.
14+
/// If null or empty - no ports are checked.
15+
/// If any required ports are reserved by other processes - throws an 'PortsUnavailableException'.
16+
/// </summary>
17+
public ushort[] RequiredPorts { get; set; }
18+
19+
/// <summary>
20+
/// Checks whether the docker compose services have come up correctly based upon the output of docker compose
1221
/// </summary>
1322
public Func<string[], bool> CustomUpTest { get; set; }
1423

@@ -19,17 +28,17 @@ public class DockerFixtureOptions : IDockerFixtureOptions
1928
/// </summary>
2029
public string[] DockerComposeFiles { get; set; } = new[] { "docker-compose.yml" };
2130
/// <summary>
22-
/// When true this logs docker-compose output to %temp%\docker-compose-*.log
31+
/// When true this logs docker compose output to %temp%\docker-compose-*.log
2332
/// </summary>
2433
public bool DebugLog { get; set; }
2534
/// <summary>
26-
/// Arguments to append after 'docker-compose -f file.yml up'
27-
/// Default is 'docker-compose -f file.yml up' you can append '--build' if you want it to always build
35+
/// Arguments to append after 'docker compose -f file.yml up'
36+
/// Default is 'docker compose -f file.yml up' you can append '--build' if you want it to always build
2837
/// </summary>
2938
public string DockerComposeUpArgs { get; set; } = "";
3039
/// <summary>
31-
/// Arguments to append after 'docker-compose -f file.yml down'
32-
/// 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
40+
/// Arguments to append after 'docker compose -f file.yml down'
41+
/// 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
3342
/// </summary>
3443
public string DockerComposeDownArgs { get; set; } = "--remove-orphans";
3544

DockerComposeFixture/Exceptions/DockerComposeException.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
namespace DockerComposeFixture.Exceptions
44
{
5-
public class DockerComposeException:Exception
5+
public class DockerComposeException : Exception
66
{
7-
public DockerComposeException(string[] loggedLines):base($"docker-compose failed - see {nameof(DockerComposeOutput)} property")
7+
public DockerComposeException(string[] loggedLines)
8+
: base($"docker compose failed - see {nameof(DockerComposeOutput)} property")
89
{
910
this.DockerComposeOutput = loggedLines;
1011
}

0 commit comments

Comments
 (0)