Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a2a1964
feat: add WaitUntilTcpConnectionIsSucceeded
hs-thimolimpert Jul 16, 2025
8ccf475
fix: naming
hs-thimolimpert Jul 16, 2025
9963856
chore: fix naming of test
hs-thimolimpert Jul 28, 2025
6f94096
chore: improve docs
Jul 28, 2025
0e54a69
chore: implement HostPortStrategy
hs-thimolimpert Aug 1, 2025
d7a81fd
chore: reflect tcp only in name and reduce code duplication
hs-thimolimpert Aug 4, 2025
f4c85fe
test: skip to let peple know, this test was thought of
hs-thimolimpert Aug 4, 2025
859b65f
fix: test probably time outs. Maybe because the listener is opened, b…
hs-thimolimpert Aug 4, 2025
a2b75a6
debug: verify that it is actually the wait that is broken
hs-thimolimpert Aug 4, 2025
4a05fb3
fix: when a test hangs we should timeout to have a better test experi…
hs-thimolimpert Aug 4, 2025
7f8abd5
fix: we need to expose the port to actually wait for it
hs-thimolimpert Aug 4, 2025
4684425
fix: don't swallow exception. When we are waiting the mapping should …
hs-thimolimpert Aug 5, 2025
df8ff45
chore: rename to Until as it is no Wait Strategy
hs-thimolimpert Aug 5, 2025
148425f
fix: we expect a throw when port not mapped
hs-thimolimpert Aug 5, 2025
db5e189
chore: Switch to internal and external TCP wait strategy
HofmeisterAn Aug 21, 2025
460b1f1
fix: Set correcct Windows command (rm copy and past error)
HofmeisterAn Aug 21, 2025
67c37f8
chore: Add remarks
HofmeisterAn Aug 21, 2025
83a65a4
chore: Refactor tests
HofmeisterAn Aug 21, 2025
0d70ba1
chore: Remove BOM
HofmeisterAn Aug 21, 2025
c50a354
docs: Add section for TCP port is available
HofmeisterAn Aug 21, 2025
89dc29b
fix: Listen on public interfaces
HofmeisterAn Aug 21, 2025
432a7a0
fix: Dont use privileged port
HofmeisterAn Aug 21, 2025
62caa85
fix: Use correct base class UntilWindowsCommandIsCompleted
HofmeisterAn Aug 21, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,40 @@ public interface IWaitForContainerOS
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
[Obsolete("Use UntilInternalTcpPortIsAvailable or UntilExternalTcpPortIsAvailable instead. This method corresponds to the internal variant.")]
IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until a TCP port is available from within the container itself.
/// This verifies that a service inside the container is listening on the specified port.
/// </summary>
/// <param name="containerPort">The TCP port of the service running inside the container.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until a TCP port is available from the test host to the container.
/// This verifies that the port is exposed and reachable externally.
/// </summary>
/// <remarks>
/// This does not necessarily mean that the TCP connection to the service running inside
/// the container was successful. For container runtimes like Docker Desktop, Podman, or similar,
/// this usually only indicates that the port has been mapped and that a connection could be
/// established to the host-side proxy that maps the port.
///
/// This wait strategy is particularly useful for container runtimes that may take some time
/// to finish setting up port mappings. In some cases, other strategies such as log-based
/// readiness checks may indicate readiness before the runtime has fully configured the port
/// mapping, leading to connection failures. This strategy helps to avoid that race condition.
/// </remarks>
/// <param name="containerPort">The TCP port of the service running inside the container.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilExternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the file exists.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace DotNet.Testcontainers.Configurations
{
using System.Net.Sockets;
using System.Threading.Tasks;
using DotNet.Testcontainers.Containers;

internal class UntilExternalTcpPortIsAvailable : IWaitUntil
{
private readonly int _containerPort;

public UntilExternalTcpPortIsAvailable(int containerPort)
{
_containerPort = containerPort;
}

public async Task<bool> UntilAsync(IContainer container)
{
var hostPort = container.GetMappedPublicPort(_containerPort);

var tcpClient = new TcpClient();

try
{
await tcpClient.ConnectAsync(container.Hostname, hostPort)
.ConfigureAwait(false);

return true;
}
catch
{
return false;
}
finally
{
tcpClient.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace DotNet.Testcontainers.Configurations
{
internal class UntilUnixPortIsAvailable : UntilUnixCommandIsCompleted
internal class UntilInternalTcpPortIsAvailableOnUnix : UntilUnixCommandIsCompleted
{
public UntilUnixPortIsAvailable(int port)
: 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))
public UntilInternalTcpPortIsAvailableOnUnix(int containerPort)
: 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))
{
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace DotNet.Testcontainers.Configurations
{
internal class UntilInternalTcpPortIsAvailableOnWindows : UntilUnixCommandIsCompleted
{
public UntilInternalTcpPortIsAvailableOnWindows(int containerPort)
: base($"Exit(-Not((Test-NetConnection -ComputerName 'localhost' -Port {containerPort}).TcpTestSucceeded))")
{
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ protected WaitForContainerOS()
/// <inheritdoc />
public abstract IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null);

/// <inheritdoc />
public abstract IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null);

/// <inheritdoc />
public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action<IWaitStrategy> waitStrategyModifier = null)
{
Expand All @@ -44,6 +47,12 @@ public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, A
return this;
}

/// <inheritdoc />
public IWaitForContainerOS UntilExternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilExternalTcpPortIsAvailable(containerPort), waitStrategyModifier);
}

/// <inheritdoc />
public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action<IWaitStrategy> waitStrategyModifier = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string>
/// <inheritdoc />
public override IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port), waitStrategyModifier);
return UntilInternalTcpPortIsAvailable(port, waitStrategyModifier);
}

/// <inheritdoc />
public override IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilInternalTcpPortIsAvailableOnUnix(containerPort), waitStrategyModifier);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string>
/// <inheritdoc />
public override IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilWindowsPortIsAvailable(port), waitStrategyModifier);
return UntilInternalTcpPortIsAvailable(port, waitStrategyModifier);
}

/// <inheritdoc />
public override IWaitForContainerOS UntilInternalTcpPortIsAvailable(int containerPort, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilInternalTcpPortIsAvailableOnWindows(containerPort), waitStrategyModifier);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,31 @@ public UntilCommandIsCompleted()
}

[UsedImplicitly]
public sealed class UntilPortIsAvailable : WindowsContainerTest
public sealed class UntilInternalTcpPortIsAvailable : WindowsContainerTest
{
public UntilPortIsAvailable()
public UntilInternalTcpPortIsAvailable()
: base(new ContainerBuilder()
.WithImage(CommonImages.ServerCore)
.WithEntrypoint("PowerShell", "-NoLogo", "-Command")
.WithCommand("$tcpListener = [System.Net.Sockets.TcpListener]80; $tcpListener.Start(); Start-Sleep -Seconds 120")
.WithCommand("$tcpListener = [System.Net.Sockets.TcpListener]80; $tcpListener.Start();$client = $tcpListener.AcceptTcpClient(); Start-Sleep -Seconds 120")
.WithWaitStrategy(Wait.ForWindowsContainer().UntilPortIsAvailable(80))
.Build())
{
}
}

[UsedImplicitly]
public sealed class UntilExternalTcpPortIsAvailable : WindowsContainerTest
{
public UntilExternalTcpPortIsAvailable()
: base(new ContainerBuilder()
.WithImage(CommonImages.ServerCore)
.WithPortBinding(80, true)
.WithEntrypoint("PowerShell", "-NoLogo", "-Command")
.WithCommand("$tcpListener = [System.Net.Sockets.TcpListener]80; $tcpListener.Start();$client = $tcpListener.AcceptTcpClient(); Start-Sleep -Seconds 120")
.WithWaitStrategy(Wait.ForWindowsContainer().UntilExternalTcpPortIsAvailable(80))
.Build())
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace DotNet.Testcontainers.Tests.Unit
{
using System;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Commons;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
using Xunit;

public sealed class WaitUntilExternalTcpPortIsAvailable : IAsyncLifetime
{
private const ushort ListeningPort = 49152;

private const ushort MappedPort = 49153;

private const ushort UnmappedPort = 49154;

private readonly IContainer _container = new ContainerBuilder()
.WithImage(CommonImages.Socat)
.WithCommand("-v")
.WithCommand($"TCP-LISTEN:{ListeningPort},crlf,reuseaddr,fork")
.WithCommand("EXEC:cat")
.WithPortBinding(ListeningPort, true)
.WithPortBinding(MappedPort, true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilExternalTcpPortIsAvailable(ListeningPort))
.Build();

public async ValueTask InitializeAsync()
{
await _container.StartAsync()
.ConfigureAwait(false);
}

public ValueTask DisposeAsync()
{
return _container.DisposeAsync();
}

[Fact]
public async Task SucceedsWhenPortIsMappedAndListening()
{
// Given
var waitStrategy = new UntilExternalTcpPortIsAvailable(ListeningPort);

// When
var success = await waitStrategy.UntilAsync(_container)
.ConfigureAwait(true);

// Then
Assert.True(success);
}

[Fact]
public async Task SucceedsWhenPortIsMappedButNotListening()
{
// Given
var waitStrategy = new UntilExternalTcpPortIsAvailable(MappedPort);

// When
var success = await waitStrategy.UntilAsync(_container)
.ConfigureAwait(true);

// Then
Assert.True(success);
}

[Fact]
public Task ThrowsWhenPortIsNotMapped()
{
var waitStrategy = new UntilExternalTcpPortIsAvailable(UnmappedPort);
return Assert.ThrowsAsync<InvalidOperationException>(() => waitStrategy.UntilAsync(_container));
}
}
}
Loading