Skip to content

Commit ae19f4d

Browse files
feat: Add wait strategy to check external (TCP) port availability (#1495)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent 65a7078 commit ae19f4d

File tree

12 files changed

+233
-21
lines changed

12 files changed

+233
-21
lines changed

docs/api/wait_strategies.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Wait strategies are useful to detect if a container is ready for testing (i.e.,
44

55
```csharp
66
_ = Wait.ForUnixContainer()
7-
.UntilPortIsAvailable(80)
7+
.UntilInternalTcpPortIsAvailable(80)
88
.UntilFileExists("/tmp/foo")
99
.UntilFileExists("/tmp/bar")
1010
.UntilOperationIsSucceeded(() => true, 1)
@@ -51,6 +51,36 @@ _ = Wait.ForUnixContainer()
5151
.ForStatusCodeMatching(statusCode => statusCode >= HttpStatusCode.OK && statusCode < HttpStatusCode.MultipleChoices));
5252
```
5353

54+
## Wait until a TCP port is available
55+
56+
Testcontainers provides two distinct strategies for waiting until a TCP port becomes available, each serving different purposes depending on your testing needs.
57+
58+
### Wait until an internal TCP port is available
59+
60+
`UntilInternalTcpPortIsAvailable(int)` checks if a service inside the container is listening on the specified port by testing connectivity from within the container itself. This strategy verifies that your application or service has actually started and is ready to accept connections.
61+
62+
```csharp
63+
_ = Wait.ForUnixContainer()
64+
.UntilInternalTcpPortIsAvailable(8080);
65+
```
66+
67+
!!!note
68+
69+
Just because a service is listening on the internal TCP port does not necessarily mean it is fully ready to handle requests. Often, wait strategies such as checking for specific log messages or verifying a health endpoint provide more reliable confirmation that the service is operational.
70+
71+
### Wait until an external TCP port is available
72+
73+
`UntilExternalTcpPortIsAvailable(int)` checks if the port is accessible from the test host to the container. This verifies that the port mapping has been established and the port is reachable externally.
74+
75+
```csharp
76+
_ = Wait.ForUnixContainer()
77+
.UntilExternalTcpPortIsAvailable(8080);
78+
```
79+
80+
!!!note
81+
82+
External TCP port availability doesn't guarantee that the actual service inside the container is ready to handle requests. It only confirms that the port mapping is established and a connection can be made to the host-side proxy.
83+
5484
## Wait until the container is healthy
5585

5686
If the Docker image supports Dockers's [HEALTHCHECK][docker-docs-healthcheck] feature, like the following configuration:

docs/examples/aspnet.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ _weatherForecastContainer = new ContainerBuilder()
106106
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", WeatherForecastImage.CertificateFilePath)
107107
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", WeatherForecastImage.CertificatePassword)
108108
.WithEnvironment("ConnectionStrings__DefaultConnection", connectionString)
109-
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(WeatherForecastImage.HttpsPort))
109+
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(WeatherForecastImage.HttpsPort))
110110
.Build();
111111
```
112112

src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,40 @@ public interface IWaitForContainerOS
6565
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
6666
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
6767
[PublicAPI]
68+
[Obsolete("Use UntilInternalTcpPortIsAvailable or UntilExternalTcpPortIsAvailable instead. This method corresponds to the internal variant.")]
6869
IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null);
6970

71+
/// <summary>
72+
/// Waits until a TCP port is available from within the container itself.
73+
/// This verifies that a service inside the container is listening on the specified port.
74+
/// </summary>
75+
/// <param name="containerPort">The TCP port of the service running inside the container.</param>
76+
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
77+
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
78+
[PublicAPI]
79+
IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);
80+
81+
/// <summary>
82+
/// Waits until a TCP port is available from the test host to the container.
83+
/// This verifies that the port is exposed and reachable externally.
84+
/// </summary>
85+
/// <remarks>
86+
/// This does not necessarily mean that the TCP connection to the service running inside
87+
/// the container was successful. For container runtimes like Docker Desktop, Podman, or similar,
88+
/// this usually only indicates that the port has been mapped and that a connection could be
89+
/// established to the host-side proxy that maps the port.
90+
///
91+
/// This wait strategy is particularly useful for container runtimes that may take some time
92+
/// to finish setting up port mappings. In some cases, other strategies such as log-based
93+
/// readiness checks may indicate readiness before the runtime has fully configured the port
94+
/// mapping, leading to connection failures. This strategy helps to avoid that race condition.
95+
/// </remarks>
96+
/// <param name="containerPort">The TCP port of the service running inside the container.</param>
97+
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
98+
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
99+
[PublicAPI]
100+
IWaitForContainerOS UntilExternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);
101+
70102
/// <summary>
71103
/// Waits until the file exists.
72104
/// </summary>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Net.Sockets;
4+
using System.Threading.Tasks;
5+
using DotNet.Testcontainers.Containers;
6+
7+
internal class UntilExternalTcpPortIsAvailable : IWaitUntil
8+
{
9+
private readonly int _containerPort;
10+
11+
public UntilExternalTcpPortIsAvailable(int containerPort)
12+
{
13+
_containerPort = containerPort;
14+
}
15+
16+
public async Task<bool> UntilAsync(IContainer container)
17+
{
18+
var hostPort = container.GetMappedPublicPort(_containerPort);
19+
20+
var tcpClient = new TcpClient();
21+
22+
try
23+
{
24+
await tcpClient.ConnectAsync(container.Hostname, hostPort)
25+
.ConfigureAwait(false);
26+
27+
return true;
28+
}
29+
catch
30+
{
31+
return false;
32+
}
33+
finally
34+
{
35+
tcpClient.Dispose();
36+
}
37+
}
38+
}
39+
}

src/Testcontainers/Configurations/WaitStrategies/UntilUnixPortIsAvailable.cs renamed to src/Testcontainers/Configurations/WaitStrategies/UntilInternalTcpPortIsAvailableOnUnix.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
namespace DotNet.Testcontainers.Configurations
22
{
3-
internal class UntilUnixPortIsAvailable : UntilUnixCommandIsCompleted
3+
internal class UntilInternalTcpPortIsAvailableOnUnix : UntilUnixCommandIsCompleted
44
{
5-
public UntilUnixPortIsAvailable(int port)
6-
: base(string.Format("true && (grep -i ':0*{0:X}' /proc/net/tcp* || nc -vz -w 1 localhost {0:D} || /bin/bash -c '</dev/tcp/localhost/{0:D}')", port))
5+
public UntilInternalTcpPortIsAvailableOnUnix(int containerPort)
6+
: base(string.Format("true && (grep -i ':0*{0:X}' /proc/net/tcp* || nc -vz -w 1 localhost {0:D} || /bin/bash -c '</dev/tcp/localhost/{0:D}')", containerPort))
77
{
88
}
99
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
internal class UntilInternalTcpPortIsAvailableOnWindows : UntilWindowsCommandIsCompleted
4+
{
5+
public UntilInternalTcpPortIsAvailableOnWindows(int containerPort)
6+
: base($"Exit(-Not((Test-NetConnection -ComputerName 'localhost' -Port {containerPort}).TcpTestSucceeded))")
7+
{
8+
}
9+
}
10+
}

src/Testcontainers/Configurations/WaitStrategies/UntilWindowsPortIsAvailable.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ protected WaitForContainerOS()
3030
/// <inheritdoc />
3131
public abstract IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null);
3232

33+
/// <inheritdoc />
34+
public abstract IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);
35+
3336
/// <inheritdoc />
3437
public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action<IWaitStrategy> waitStrategyModifier = null)
3538
{
@@ -44,6 +47,12 @@ public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, A
4447
return this;
4548
}
4649

50+
/// <inheritdoc />
51+
public IWaitForContainerOS UntilExternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null)
52+
{
53+
return AddCustomWaitStrategy(new UntilExternalTcpPortIsAvailable(containerPort), waitStrategyModifier);
54+
}
55+
4756
/// <inheritdoc />
4857
public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action<IWaitStrategy> waitStrategyModifier = null)
4958
{

src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string>
2828
/// <inheritdoc />
2929
public override IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null)
3030
{
31-
return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port), waitStrategyModifier);
31+
return UntilInternalTcpPortIsAvailable(port, waitStrategyModifier);
32+
}
33+
34+
/// <inheritdoc />
35+
public override IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null)
36+
{
37+
return AddCustomWaitStrategy(new UntilInternalTcpPortIsAvailableOnUnix(containerPort), waitStrategyModifier);
3238
}
3339
}
3440
}

src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string>
2828
/// <inheritdoc />
2929
public override IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null)
3030
{
31-
return AddCustomWaitStrategy(new UntilWindowsPortIsAvailable(port), waitStrategyModifier);
31+
return UntilInternalTcpPortIsAvailable(port, waitStrategyModifier);
32+
}
33+
34+
/// <inheritdoc />
35+
public override IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null)
36+
{
37+
return AddCustomWaitStrategy(new UntilInternalTcpPortIsAvailableOnWindows(containerPort), waitStrategyModifier);
3238
}
3339
}
3440
}

0 commit comments

Comments
 (0)